From 78fa24a665002c01f0ea7ded8791c7e6af470305 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Sun, 12 Jul 2026 17:00:43 +0300 Subject: Add ggaze implementation plan (docs/) --- docs/PLAN.md | 230 ++++++++++++++++++++++++++ docs/README.md | 55 +++++++ docs/architecture.md | 242 +++++++++++++++++++++++++++ docs/coding-conventions.md | 127 +++++++++++++++ docs/gegl.md | 150 +++++++++++++++++ docs/goals-and-scope.md | 102 ++++++++++++ docs/open-questions.md | 238 +++++++++++++++++++++++++++ docs/roadmap.md | 120 ++++++++++++++ docs/tech-stack.md | 109 +++++++++++++ docs/ui-and-interactions.md | 390 ++++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 1763 insertions(+) create mode 100644 docs/PLAN.md create mode 100644 docs/README.md create mode 100644 docs/architecture.md create mode 100644 docs/coding-conventions.md create mode 100644 docs/gegl.md create mode 100644 docs/goals-and-scope.md create mode 100644 docs/open-questions.md create mode 100644 docs/roadmap.md create mode 100644 docs/tech-stack.md create mode 100644 docs/ui-and-interactions.md (limited to 'docs') diff --git a/docs/PLAN.md b/docs/PLAN.md new file mode 100644 index 0000000..636f009 --- /dev/null +++ b/docs/PLAN.md @@ -0,0 +1,230 @@ +# ggaze — Project Plan (living document) + +**GNOME Gaze** — a small, fast, native image viewer for Fedora +Linux, written in C with GTK4. Its job: quickly preview a folder of pictures +downloaded from a camera, cull the rejects, move on. + +This file is the running tracker. The detailed design lives in the sibling +docs; this page keeps the overview, the decisions log, and the status of each +milestone in one place. Update it as we go. + +--- + +## Elevator pitch + +`ggaze ~/Downloads/Camera/IMG_0001.jpg` opens the folder as a thumbnail grid, +`Enter` drops into the large view, `h`/`l` (or `←`/`→`) scrubs through the shoot, `i` shows +EXIF, `d` bins a reject into `./Trash` (undoable), `D` deletes it outright, `v` marks +keepers, `m` then `1` ships them to "irregular ninja", `e` opens a keeper in GIMP, +`!` runs `usbimport` to pull new shots, `a` previews a quick GEGL auto-fix, `Esc` returns to the grid, `q` quits. +No library, no database, no sidecars. + +## Two views + +- **Grid** — thumbnail overview of the whole folder, keyboard-navigable; + thumbnails resize with `+`/`-` and the grid reflows to fit. +- **Large** — single picture with zoom/pan, fullscreen, slideshow. + +Switch with `Enter` (grid→large) and `Esc`/`Backspace` (large→grid), or `t` to +toggle. The cursor stays in sync across the switch. + +## Opening files & folders + +- `ggaze file.jpg` opens the file (large view, parent folder as navigator); + `ggaze dir/` opens the folder in the grid. +- Drag-and-drop a file or folder onto the window to open it. +- `o` opens a file/folder dialog. + +## Folder monitoring + +- `GFileMonitor` watches the current dir; external adds/deletes/moves + refresh the grid live (debounced). A removed current file falls back to the + nearest; `r` still does a manual reload. + +## Deletion model + +- `d` → move to `/.Trash/` (local, recoverable, undoable via `u`). +- `D` → permanent delete (unlink, no undo). +- `./Trash` lives with the shoot; empty it via the menu or from a shell. +- Trashed/deleted items stay listed but dimmed (hide toggle in settings). + +## Moving & marks + +- `v` / `V` / `Ctrl+a` mark pictures; marks persist across views. +- `m` → popup of **configured** destinations, each with an auto-assigned + hotkey (`1`, `2`, …). Press the hotkey to move the marked set there. +- Destinations = ordered `a(ss)` list of name → path pairs in GSettings. +- `u` undoes the last `d` or `m`. + +## External programs + +- `e` → popup of configured programs (auto-assigned hotkeys), launches the + current image in the chosen one (detached `GSubprocess`, `%f` = path). +- `editors` = ordered `a(ss)` list of name → command pairs in GSettings. + +## Shell scripts + +- `!` → popup of configured scripts (auto-assigned hotkeys), runs the chosen + one **asynchronously** via `/bin/sh -c` (`%f` = image, `%d` = folder). +- On exit, ggaze rescans the directory (e.g. `usbimport` adds files). +- `scripts` = ordered `a(ss)` list of name → command pairs in GSettings. + +## Quick enhance (GEGL, optional) + +- `a` → popup of enhance presets (auto-assigned hotkeys); applies a GEGL graph + as a **non-destructive live preview**; press again / `Esc` to turn off. +- `s` / menu *Save enhanced copy…* writes `-enhanced.`; original + untouched. **No auto-save** — navigating away from an un-exported preview + prompts Save/Discard/Cancel. +- `enhance-presets` = ordered `a(ss)` list of `(name, gegl-graph)` pairs. +- GEGL is an optional meson feature; without it, ggaze is a plain fast viewer. + GEGL also brings ICC color management. See [gegl.md](gegl.md). + +## Crop, straighten & rotate (GEGL, optional) + +- `c` crop (adjustable rect, aspect presets), `R` straighten (horizon drag / + angle nudge), and `[`/`]` rotate 90° CCW/CW — non-destructive, same preview + graph + `s` to export a copy. +- Hold `Space` to compare original vs modified (before/after); release returns + to the modified preview. +- Large view only; GEGL required. + +## Copy to clipboard + +- `Ctrl+c` / menu *Copy*: no marks → current image as PNG pixels; marks → + marked files as `text/uri-list`. Paste into Katogram/GIMP/file managers + (like gthumb). + +## Reachability (keyboard + GUI) + +Every GUI element has a hotkey, and it's shown on the element (menu items, +tooltips, popup entries). Conversely, every action is also reachable by +mouse — header-bar buttons for the common ones, the `F10` menu for the rest. +`?` lists all shortcuts. Neither keyboard nor mouse is a fallback. + +## Quality & testing + +- ≥80% unit-test coverage on plain-C modules (gcov/lcov), gated in CI. +- Run the `auditing-code-quality` skill at each milestone boundary and before + release — for C: `c-best-practices` + `find-code-bugs` + `solid-principles` + + `beyond-solid-principles`, findings tracked as tasks via + `agent-task-management`. Fix all HIGH/MEDIUM before release. + +--- + +## Tech at a glance + +| Concern | Choice | +|--------------|---------------------------------| +| Language | C11 | +| UI | GTK4 + libadwaita (decided) | +| Async/objects| GLib / GObject / GTask | +| Config | GSettings (`org.buetow.ggaze`) | +| Build | Meson + Ninja | +| Decode | GdkPixbuf fallback + libjxl/libavif/libheif (feature options) | +| Image proc. | GEGL + babl (optional, feature-gated) — enhance, ICC, export copy | +| Thumbnails | freedesktop TMS, `~/.cache/thumbnails/` | +| Packaging | Fedora RPM + AppStream (Flatpak later) | +| Testing | `meson test` + gcov/lcov; ≥80% on plain-C modules | +| Quality audit | `auditing-code-quality` skill at milestones (C: c-best-practices + find-code-bugs + SOLID + beyond-SOLID) | + +See [tech-stack.md](tech-stack.md). + +--- + +## Milestones + +| M | Title | Status | Notes | +|-----|----------------------------|----------|-------| +| M0 | Skeleton (app + empty window) | not started | Meson, GtkApplication, file/folder arg, --version/--help | +| M1 | Show one image (zoom/pan) | not started | custom viewer widget, GdkPixbuf backend, EXIF orientation on load | +| M2 | Walk the directory | not started | navigator, `h`/`l` prev/next, folder arg + drag-drop open, `GFileMonitor` auto-refresh, wrap, header counter | +| M3 | Responsive + prefetch | not started | GTask decode, 2–3 slot LRU, cancel-in-flight | +| M4 | Fullscreen + slideshow + info | not started | `f`, `s`, `i` EXIF overlay | +| M5 | Modern formats | not started | JXL/AVIF/HEIF backends, animated GIF/WebP | +| M6 | Progressive low-res preview | not started | libjpeg-turbo early low-res scan | +| M7 | Thumbnail cache + grid view | not started | TMS cache + `gridview`, dim trashed items | +| M8 | Selection, move, open-external & scripts | not started | marks, `m`/`e`/`!` popups, `mover`/`opener`/`runner`, `Ctrl+c` clipboard, destinations+editors+scripts `a(ss)`, prefs | +| M9 | GEGL quick-enhance, crop/straighten/rotate (optional) | not started | `enhancer`, `a`/`c`/`R`/`[`/`]` tools, hold-`Space` compare, non-destructive preview, `s` save copy (no auto-save, prompt on navigate), ICC via GEGL | +| M10 | Polish & packaging | not started | AppStream, RPM, man page, settings, keyboard-completeness audit, ≥80% coverage gate | + +Later / maybe: configurable keybindings, recursive walking, RAW embedded +preview, burst grouping, RAW+JPEG pair hiding, GEGL transforms/artistic. + +See [roadmap.md](roadmap.md). + +--- + +## Decisions log + +Decisions made during planning. Newest first. + +| # | Date | Decision | Rationale | +|---|------------|--------------------------------------------------------------------------|-----------------------------------| +| 28 | 2026-07-12 | Folder monitoring via `GFileMonitor` (GIO): external adds/deletes/moves refresh the grid live (debounced); removed current file falls back to nearest. | New shots from usbimport/etc. appear without manual reload. | +| 29 | 2026-07-12 | **UI toolkit: libadwaita** (was A). GNOME-native header bar/dark viewer/system theme; no theming overrides. | Native Fedora look per the gthumb-but-KISS direction. | +| 30 | 2026-07-12 | **App ID `org.buetow.ggaze`** (was B). | Matches buetow.org domain. | +| 31 | 2026-07-12 | **Custom viewer widget** (was L), not `GtkPicture`. | Cursor-centered zoom, pan clamp, hold-`Space` compare, tool overlays. | +| 32 | 2026-07-12 | **Single instance** (was E); new `open`/drop replaces current folder+image. | Standard GNOME behavior; no window sprawl. | +| 33 | 2026-07-12 | **Camera specifics** (was K): burst grouping deferred to "later"; hide RAW sidecars by default (toggle to reveal); default sort = filename (EXIF capture-time as a menu option); import folder = just a path. | KISS first; culling-friendly grid; filename ≈ shot order. | +| 34 | 2026-07-12 | **GEGL integration** (was U): optional meson feature; built-in presets programmatic, user presets as `gegl-graph` text; apply only when settled (not during scrub); "enhanced" badge; export `-enhanced.` same dir, collision suffix `-1`; dirty-prompt fires on `d`/`D`/`m`; no gegl-gtk. | Keeps core fast; KISS preset UI; explicit save. | +| 35 | 2026-07-12 | **GEGL compose order** (was W): load → enhance(color) → rotate → straighten → crop → export; straighten auto-crop default on. | Crop the final framed image; remove rotated corners. | +| 36 | 2026-07-12 | **Enhance presets** (was X+#4): one active color preset (replace); crop/straighten/rotate stack on top; "reset preview" clears all; combine color presets via one `gegl-graph` entry; curves via `gegl:contrast-curve`; no fine-adjust nudging in v1. | Predictable + KISS; combinable via graph text. | +| 37 | 2026-07-12 | **Milestone Leans locked**: C GdkPixbuf-first; G color via GEGL/sRGB-else; H scroll=zoom + `pan-when-zoomed` mode; M GSettings `a(ss)` destinations; N move+suffix; O 1-9,0,a-z (cap 36); P one-level undo; Q marks path-based/survive re-sort/clear on trash; R raw cmd+%f+GSubprocess; S /bin/sh -c single-quote+rescan; T 64-512px ±32px custom bucket; V PNG+uri-list union provider; Y EXIF normalize-to-identity, export tag=1; Z folder→grid/file→parent/many→first; AA 250ms debounce nearest; F flat default; J RPM+AppStream first. | Working defaults confirmed at each milestone. | +| 40 | 2026-07-12 | Run the `auditing-code-quality` skill at each milestone + before release (C-adapted: `c-best-practices` + `find-code-bugs` + `solid-principles` + `beyond-solid-principles`, tracked via `agent-task-management`); fix all HIGH/MEDIUM findings. | Structured, well-factored project; catch defects + design smells early. | +| 39 | 2026-07-12 | **≥80% unit-test coverage** on plain-C modules (navigator/detect/thumbnail/mover/opener/runner/enhancer/trash/settings) via gcov/lcov with a CI coverage gate; GTK widgets get smoke tests. | Quality floor; refactor safely. | +| 38 | 2026-07-12 | **Gaps folded in**: mark count in header; window-geometry persistence; CLI `--version`/`--help` (+`--sort`/`--view` later); bulk `D` confirm >1; `scroll-behavior` = zoom/pan-when-zoomed/navigate; `Ctrl+c` copies the *displayed* image; export same ext + JPEG q95 (lossless later); `e` opens the *original* file; go-to-# skipped for v1. | Plan now complete before implementation. | +| 27 | 2026-07-12 | Open a folder arg (`ggaze dir/` → grid) and accept drag-and-drop of a file/folder onto the window; `o` dialog allows folders too. | Match gthumb flexibility; open anything from CLI, file manager, or drag. | +| 26 | 2026-07-12 | Honor EXIF Orientation on load (upright display); manual rotate/straighten compose on top; export resets the tag to normal. | Portrait/tilted camera shots display correctly without manual fix. | +| 25 | 2026-07-12 | Expand GEGL enhance presets: brightness, contrast, saturation, warm/cool, white balance, shadows/highlights, levels, clarity + artistic (B&W/sepia/vignette/softglow); strength tunable via `enhance-presets` gegl-graph text (no slider UI). | Cover the common quick fixes as one-shot presets; keep KISS. | +| 24 | 2026-07-12 | Compare moved to `Space` (hold); zoom-fit folded into `0` (toggle fit/100%); `\` freed. | `Space` is the comfortable hold-to-compare key; one zoom toggle key. | +| 23 | 2026-07-12 | Hold `Space` to flash the original image; release to return to the modified preview (before/after compare) to decide whether to `s` save. | Judge edits before saving; no accidental keeps. | +| 22 | 2026-07-12 | Add 90° rotation (`[`/`]`, `gegl:rotate-on-center`) as a one-shot non-destructive GEGL transform; `s` exports a copy. | Quick orientation fix; original untouched. | +| 21 | 2026-07-12 | Add crop (`c`, `gegl:crop`) and straighten (`R`, `gegl:rotate`) as non-destructive interactive GEGL tools; `s` exports a copy. | Level horizons and frame shots without leaving ggaze; original untouched. | +| 20 | 2026-07-12 | `Ctrl+c` copies to clipboard: current image as PNG pixels, or marked files as `text/uri-list` (paste into Katogram/GIMP/etc. like gthumb). | Quick hand-off of an image/selection to other apps. | +| 19 | 2026-07-12 | Layout & design reminiscent of gthumb (header bar, thumbnail grid, full-window viewer) but KISS: no folder sidebar, no catalogs/tags, no status-bar clutter, no batch/edit toolbars. | Familiar GNOME image-app feel without the weight. | +| 18 | 2026-07-12 | No auto-save of image changes. `s` saves an enhanced copy manually; navigating away (or quitting) from an un-exported enhance preview prompts Save/Discard/Cancel. Slideshow moved to `S` to free `s` for save. | Originals never silently modified; explicit consent. | +| 17 | 2026-07-12 | Plan GEGL (optional, feature-gated) for quick non-destructive enhance (`a` popup of presets) + export copy (`s`); also brings ICC color mgmt and format save. | Judge/fix keepers in-app without a full editor; original never modified. | +| 16 | 2026-07-12 | Grid thumbnails are resizable (`+`/`-`); grid auto-reflows to fit; size persisted in `thumbnail-size` GSettings. | Overview at a glance vs. detail, ad hoc. | +| 15 | 2026-07-12 | `!` runs configurable shell scripts asynchronously via `/bin/sh -c` (`%f`/`%d`), rescan dir on exit; `scripts` `a(ss)` settings. | Run usbimport etc. from within ggaze; pick up new files. | +| 14 | 2026-07-12 | `e` opens the current image in a configurable external program via a popup (auto-assigned hotkeys); `editors` `a(ss)` settings; detached GSubprocess launch. | Hand off to GIMP/identify/etc. without leaving ggaze. | +| 13 | 2026-07-12 | UI is self-documenting: hotkeys shown on elements, tooltips, `?` overlay, badges/counters/toasts narrate state. | Discoverable without a manual. | +| 12 | 2026-07-12 | Every action is also reachable through the GUI (button/menu), not only by hotkey. | Keyboard and mouse are equally first-class. | +| 11 | 2026-07-12 | Hotkeys are shown on the elements themselves (menu items, tooltips, popup entries). | Discoverability; no hidden keys. | +| 10 | 2026-07-12 | Every GUI element has a hotkey / is keyboard-reachable; full mnemonics. | Fully keyboard-driven, no mouse needed. | +| 9 | 2026-07-12 | `m` moves marked pictures to a configured destination via a popup with auto-assigned hotkeys; destinations user-configurable; multi-select via marks. | Fast triage of camera dumps into named folders. | +| 8 | 2026-07-12 | Follow the c-best-practices skill; conventions pinned in coding-conventions.md. | Consistent C style across the project. | +| 7 | 2026-07-12 | vi-style nav (`h`/`l` prev/next) plus cursor keys (`←`/`→`); `j`/`k` pan when zoomed. | vi users + cursor fallback. | +| 6 | 2026-07-12 | `D` permanently deletes; no undo. | Fast path for obvious garbage. | +| 5 | 2026-07-12 | `d` moves to a local `./Trash` folder, not the system trash; undoable. | Trash travels with the shoot; easy to inspect/empty. | +| 4 | 2026-07-12 | Two views: thumbnail grid + large single-picture, in one window. | Overview + detail, both keyboard-driven. | +| 3 | 2026-07-12 | Aim = quickly preview camera downloads and cull them. | Narrows scope to a culling viewer. | +| 2 | 2026-07-12 | Stack: C + GTK4, native Fedora/GNOME look. | User requirement. | +| 1 | 2026-07-12 | Planning docs only first; no implementation yet. | Get the design straight first. | + +Most open questions are now **decided** (see decisions #29–#38); remaining +working Leans and per-milestone details live in +[open-questions.md](open-questions.md). + +--- + +## Documents index + +- [README.md](README.md) — overview + working assumptions +- [goals-and-scope.md](goals-and-scope.md) — goals, non-goals, target workflow +- [architecture.md](architecture.md) — modules, data flow, concurrency +- [ui-and-interactions.md](ui-and-interactions.md) — views, keybindings, gestures, grid +- [tech-stack.md](tech-stack.md) — libraries, decode backends, deps +- [coding-conventions.md](coding-conventions.md) — C style (c-best-practices skill) +- [gegl.md](gegl.md) — GEGL quick-enhance & image-processing plan +- [roadmap.md](roadmap.md) — milestone detail +- [open-questions.md](open-questions.md) — undecided items +- `PLAN.md` — this file (tracker) + +## How to use this file + +- Flip milestone **Status** as work starts/finishes. +- Add a row to the **Decisions log** whenever something is settled (and move + the matching item out of `open-questions.md`). +- Keep the elevator pitch and tables in sync with the detail docs; if they + disagree, the detail docs are authoritative and this file gets updated. \ No newline at end of file diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..b124b12 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,55 @@ +# ggaze — GNOME Gaze + +A small, fast, native image viewer for Fedora Linux, written in C with GTK4. + +The aim is concrete: **quickly preview a folder of pictures downloaded from a +camera.** You pull a shoot off the camera into a directory, fire up ggaze on +any file, and flip through the lot fast — glance at EXIF to tell near-identical +frames apart, trash the obvious rejects, move on. It is a culling viewer, not +a library manager. + +The emphasis is on **quick**: fast startup, instant first-frame display, +keyboard-driven navigation through a directory of images, and a UI that gets +out of the way. Think of it as a modern, GNOME-native take on the classic +`feh` / `nsxiv` / `qiv` lineage — minimal chrome, fast decode, no library +management bloat. + +Layout-wise it nods to **gthumb** (header bar, thumbnail grid, full-window +viewer) but stays KISS — no sidebar, no catalogs, no toolbars. + +This folder contains **planning only**. No implementation yet. + +## Documents + +- [goals-and-scope.md](goals-and-scope.md) — what ggaze is and is not +- [architecture.md](architecture.md) — module layout and data flow +- [ui-and-interactions.md](ui-and-interactions.md) — window layout, keybindings, gestures +- [tech-stack.md](tech-stack.md) — libraries, build system, dependencies +- [coding-conventions.md](coding-conventions.md) — C style (follows the c-best-practices skill) +- [gegl.md](gegl.md) — GEGL quick-enhance & image-processing plan +- [roadmap.md](roadmap.md) — milestones from skeleton to polish +- [open-questions.md](open-questions.md) — decisions still to be made + +## One-line summary + +`ggaze ~/Downloads/Camera/IMG_0001.jpg` — opens instantly in a thumbnail +grid of the folder, `Enter` into the large view, walk the shoot with `h`/`l` (or `←`/`→`), +check EXIF with `i`, `d` to bin a reject into `./Trash`, `D` to delete it +outright, `q` to leave. + +## Command-line + +- `ggaze [FILE|FOLDER]` — open a file (large view) or a folder (grid). +- `--version`, `--help`. +- `--sort=name|time|size`, `--view=grid|large` (convenience/scripting; later). + +## Working assumptions (correct me) + +- Desktop app, single main window with **two views**: a thumbnail grid + (overview of the folder) and a large single-picture view. +- Rejected pictures go into a `./Trash` folder beside the images (local, + recoverable), not the system trash. `D` deletes permanently. +- Opens a file **or** a folder (CLI arg or drag-and-drop); `ggaze dir/` → grid. +- Primary platform: Fedora Linux / GNOME. Other GTK4 platforms are a bonus. +- App ID / GApplication: `org.buetow.ggaze` (placeholder). +- Build system: Meson (GNOME/Fedora convention). \ No newline at end of file diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..b747199 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,242 @@ +# Architecture + +Layered: a thin GTK shell over a small set of focused C modules. Each module +has one job and a narrow interface; the UI never calls decoders directly. + +## Module sketch + +``` +ggaze +├── main.c # entry point, CLI arg parsing, GtkApplication setup +├── app/ # GApplication, GActions (open, quit, prefs), single-instance +├── window.{c,h} # GgazeWindow : GtkApplicationWindow — owns the layout, switches grid/large +├── viewer.{c,h} # GgazeViewer : GtkWidget — large single-image canvas, zoom/pan, displays a GdkTexture +├── gridview.{c,h} # GgazeGrid : GtkGridView/FlowLayout — thumbnail overview of the folder +├── trash.{c,h} # ./Trash folder management + permanent delete; restore/undo +├── mover.{c,h} # configurable move destinations; move marked set into a dir (undoable) +├── opener.{c,h} # configurable external programs; launch current image (GSubprocess) +├── runner.{c,h} # configurable shell scripts; async run via /bin/sh -c, rescan on done +├── enhancer.{c,h} # (optional) GEGL quick-enhance presets; non-destructive apply + export copy +├── clipboard.{c,h} # copy image (PNG) or file URIs to GdkClipboard (no state, helpers) +├── loader/ +│ ├── loader.{c,h} # async load API: load(path, cancellable, ready_cb) +│ ├── detect.{c,h} # sniff format from contents (magic), not extension +│ └── backends/ # one file per format family, behind a backend struct +│ ├── pixbuf.c # fallback via GdkPixbuf (PNG/JPEG/GIF/WebP) +│ ├── jxl.c # libjxl +│ ├── avif.c # libavif +│ └── heif.c # libheif +├── navigator.{c,h} # directory listing, sort, filter, prev/next, wrap, recurse(opt) +├── thumbnail.{c,h} # freedesktop thumbnail cache (normal/large), shared/mutex +├── settings.{c,h} # GSettings schema wrapper +└── shortcuts.{c,h} # keybinding → GAction map (configurable later) +``` + +## Responsibilities + +- **app** — owns the `GtkApplication`, registers actions, handles the `open` + signal (files **or a directory** → window), single-instance behavior. A + directory arg opens the folder in the grid; a file arg opens its parent + folder with that file current. +- **window** — owns the two view modes (**grid** and **large**) in a + `GtkStack`, the header bar, and the info overlay. Routes actions to + navigator/loader/viewer/gridview; manages fullscreen state. Keeps the + navigator cursor in sync so switching grid↔large preserves position. Tracks + the enhance "dirty" flag and gates navigation on it (prompt + Save/Discard/Cancel when an un-exported enhance preview is active). Hosts + interactive tool overlays (crop, straighten) in large view. Has a + `GtkDropTarget` accepting dropped files/folders (open them). +- **viewer** — the *large* view. Pure display widget. Takes a `GdkTexture` + (or `GtkSnapshot` paintable). Owns zoom level, pan offset, fit mode. Draws + via GTK4 render nodes. Holds both the raw and GEGL-processed textures; + `Space` swaps to the raw (compare) while held. Emits "needs-next" when nearing + the end of a preloaded set. +- **gridview** — the *thumbnail* view. A `GtkGridView` (or `GtkFlowBox`) + backed by a `GListModel` of the navigator's files, each cell rendered from + the `thumbnail` cache. Thumbnail size is adjustable (`+`/`-`); cells reflow + to fit the window. Size comes from GSettings `thumbnail-size`. Selection + follows the navigator cursor. Double-click / `Enter` switches to large view + on the selected item. +- **loader** — runs decode in a `GTask` thread, returns a `GdkTexture` on the + main thread. Format detection by content sniffing. **Applies EXIF + Orientation** so the texture is upright (GdkPixbuf path: + `gdk_pixbuf_apply_embedded_orientation`; other backends read the EXIF tag and + rotate/flip). Backend selected at build time via meson `feature` options. +- **navigator** — given a starting file, lists the parent directory, filters + to image MIME types, sorts (name/time/size), exposes `current/prev/next`. + Also owns the **mark set** (multi-select): `navigator_toggle_mark`, + `navigator_mark_range`, `navigator_mark_all`, `navigator_clear_marks`, + `navigator_get_marks` (returns a `GList` of `GFile*`). Emits a `changed` + signal on sort/filter/trash/move so grid + large stay in sync. Watches the + directory with `GFileMonitor` and emits `changed` on external + adds/deletes/moves (debounced); if the current file is removed, falls back + to the nearest. Owns no GTK state; testable standalone. +- **trash** — moves a file to `/.Trash/` (creating it lazily), preserving + relative path uniqueness (suffix `-1`, `-2`… on collision). `D` calls + `g_file_delete` instead. Tracks the last trashed item for `u` undo/restore. + Never touches the system trash. +- **mover** — owns the configured destination list (loaded from settings) and + performs `g_file_move` for a set of `GFile*` into a chosen destination, with + collision suffixing. Records the last move (paths + dest) so `u` can move + them back. Exposes `mover_get_dests` (ordered, for the popup + hotkey + assignment) and `mover_move(GList *paths, MoverDest *dest, GError **)`. +- **opener** — owns the configured external-program list (loaded from + settings). Expands `%f` (and later `%F`) in the command and launches it + detached via `GSubprocess` (`g_subprocess_new`). Exposes + `opener_get_progs` (ordered, for the popup + hotkey assignment) and + `opener_launch(GFile *file, OpenerProg *prog, GError **)`. Owns no GTK + state; the window owns the popup. +- **runner** — owns the configured shell-script list (loaded from settings). + Expands `%f` (current image) and `%d` (current folder) in the command and + runs it **asynchronously** via `/bin/sh -c` (`GSubprocess` with + `g_subprocess_wait_async`); substituted paths are single-quoted to prevent + shell injection. Exposes `runner_get_scripts` (ordered, for the popup + + hotkey assignment) and `runner_run(GFile *file, GFile *dir, + RunnerScript *script, GAsyncReadyCallback on_done, GError **)`. On + completion the window calls `navigator_rescan()` (scripts may mutate the + folder) and shows a toast with the exit status. Owns no GTK state. +- **enhancer** *(optional, if GEGL is enabled)* — owns the enhance-preset list + (loaded from settings). Builds a GEGL op graph for a preset and applies it + to a `GeglBuffer` in a `GTask` thread: `enhancer_get_presets`, + `enhancer_apply(GeglBuffer *in, EnhancerPreset *, GError **) → GeglBuffer*`, + `enhancer_export(GeglBuffer *in, EnhancerPreset *, GFile *out, GError **)`. + The window imports the decoded image into a `GeglBuffer` when a preset is + active and renders the result back to a `GdkTexture`. The crop/straighten/ + rotate tools add `gegl:crop`/`gegl:rotate`/`gegl:rotate-on-center` to the same + graph via the enhancer. GEGL also backs color-managed decode/export (ICC). + Owns no GTK state. +- **clipboard** — stateless helpers that put content on the `GdkClipboard`: + `clipboard_copy_image(GdkClipboard *clip, GFile *file, GCancellable *, + GError **)` decodes the image in a `GTask` thread and sets a + `GdkContentProvider` for `image/png`; `clipboard_copy_uris(GdkClipboard + *clip, GList *files)` sets `text/uri-list` (+ `text/plain`). Single image → + pixels; marks → URIs. (Optionally union both providers so one file offers + PNG + URI.) +- **thumbnail** — reads/writes `~/.cache/thumbnails/` per the freedesktop + Thumbnail Managing Standard; shared so multiple windows don't re-decode. + Also feeds the gridview cells. +- **settings** — wraps a `GSettings` schema: sort order, wrap, background + colour, scroll behavior (zoom vs navigate), slideshow delay, + `thumbnail-size` (grid thumbnail pixel size), hide-trashed toggle, + `destinations` — an ordered `a(ss)` array of `(name, path)` pairs + used by the move popup, and `editors` — an ordered `a(ss)` array of + `(name, command)` pairs used by the `e` open-in popup (`%f` = current path), + and `scripts` — an ordered `a(ss)` array of `(name, command)` pairs used by + the `!` run-script popup (`%f` = current path, `%d` = current folder; run + via `/bin/sh -c`), and `enhance-presets` — an ordered `a(ss)` array of + `(name, gegl-graph)` pairs for the `a` enhance popup (GEGL only). List + order = hotkey order (`1`, `2`, …). + +## Data flow (next image, large view) + +``` +key 'l' → window action "next" + → navigator.next() → path2 + → loader.load(path2, cancellable) [thread] + → GdkTexture ready [main thread] + → viewer.set_texture(texture) + → thumbnail.ensure(path2) [background] +``` + +## Data flow (grid ↔ large) + +``` +grid Enter / double-click → window.set_view(LARGE) + → navigator.set_current(selected_path) + → viewer shows that image +large Esc / Backspace → window.set_view(GRID) + → gridview scrolls cursor into view, focused +``` + +## Data flow (trash / delete) + +``` +key 'd' → trash.bin(path) → mv path → /.Trash/ (undoable) +key 'D' → trash.delete(path) → unlink(path) (not undoable) + → navigator.remove(path) → emits 'changed' + → gridview drops cell / dims it; large view advances to next +``` + +## Data flow (move) + +``` +key 'm' → window shows move popup (GtkPopover) + → mover_get_dests() → [ {"irregular ninja", ~/…}, {"alt …", …}, … ] + → popup assigns hotkeys 1..9,0,a.. by list order +key '2' → mover_move(marked_paths, dests[1], &err) + → g_file_move each (rename/copy+delete), collision-suffix + → navigator.remove(each) → emits 'changed' + → grid drops cells; large advances; counter updates + → mover records move for 'u' undo +no marks? → move acts on navigator.current instead +``` + +## Data flow (open in external program) + +``` +key 'e' → window shows open-in popup (GtkPopover) + → opener_get_progs() → [ {"GIMP", "gimp %f"}, {"identify", …}, … ] + → popup assigns hotkeys 1..9,0,a.. by list order +key '2' → opener_launch(current_path, progs[1], &err) + → expand %f → argv; g_subprocess_new (detached) + → toast on failure; ggaze stays responsive, image stays open +``` + +## Data flow (run shell script) + +``` +key '!' → window shows scripts popup (GtkPopover) + → runner_get_scripts() → [ {"usbimport", "~/scripts/usbimport %d"}, … ] + → popup assigns hotkeys 1..9,0,a.. by list order +key '1' → runner_run(current_path, dir, scripts[0], on_done, &err) + → expand %f/%d (single-quoted) → /bin/sh -c "" + → g_subprocess_wait_async; ggaze stays responsive; toast: "running…" +on done → navigator_rescan() (scripts may add/remove files) + → toast: "usbimport finished (exit 0)" or error +``` + +## Data flow (quick enhance, GEGL) + +``` +key 'a' → window shows enhance popup (GtkPopover) + → enhancer_get_presets() → [ {"Auto-fix", "stretch-contrast|color-enhance"}, … ] + → popup assigns hotkeys 1..9,0,a.. by list order +key '1' → import decoded image → GeglBuffer + → enhancer_apply(buf, presets[0], &err) [GTask thread] + → GeglBuffer out → render to GdkTexture → viewer (non-destructive) + → toggle off on second press / Esc +key 's' → enhancer_export(buf, presets[0], out_file, &err) + → writes IMG_0001-enhanced. via GEGL saver; original untouched + → clears the dirty flag for this image +navigate with dirty preview → prompt: Save (export) / Discard / Cancel +GEGL disabled? → 'a' shows "GEGL not built in" toast +``` + +## Data flow (copy to clipboard) + +``` +Ctrl+c → marks? clipboard_copy_uris(clip, marked_files) [text/uri-list] + → no marks? clipboard_copy_image(clip, current, cancellable, &err) + → decode in GTask → GdkPixbuf/Texture → PNG content provider + → gdk_clipboard_set_content (main thread) + → toast: "Copied image" / "Copied N files" +``` + +Prefetch: when `navigator.current` changes, schedule `loader.load` for the +*next* and *previous* paths into a small (2–3 slot) texture cache so navigation +feels instant. + +## Concurrency model + +- Only the main thread touches GTK widgets. +- Decode happens in `GTask` worker threads (one at a time per load, with a + `GCancellable` so a rapid `jjjj` cancels stale work). +- Thumbnail I/O on a low-priority thread or `GThreadPool`. +- A bounded LRU of decoded `GdkTexture`s (e.g. 4) to bound memory on large + folders / huge images. + +## Threading / cancellation invariant + +At most one *active* load per window. Issuing a new load cancels the previous +cancellable and drops its result. The viewer only ever shows a texture whose +path matches `navigator.current`. \ No newline at end of file diff --git a/docs/coding-conventions.md b/docs/coding-conventions.md new file mode 100644 index 0000000..e1d1d44 --- /dev/null +++ b/docs/coding-conventions.md @@ -0,0 +1,127 @@ +# Coding Conventions + +ggaze follows the **c-best-practices** skill +(`~/.agents/skills/c-best-practices/SKILL.md`). That skill and its references +are authoritative; this page summarizes the conventions as they apply to +ggaze so contributors don't have to context-switch. When in doubt, the skill +wins. + +## Source style + +- **Indentation**: 3 spaces, no tabs. +- **Line length**: max 80 chars. +- **Braces**: K&R — opening brace on the same line as the statement/condition. +- **Pointer asterisk**: on the variable, not the type — `Token *p_token`, not + `Token* p_token`. +- **Returns**: parenthesized — `return (p_token);`. Return type on its own + line above the function name. +- **Comments**: `/* ... */` for blocks, `//` for single lines; put notes on + their own line, not trailing. + +## Naming + +| Category | Convention | ggaze examples | +|-------------------|-----------------------------|------------------------------------------| +| Types | PascalCase | `GgazeWindow`, `GgazeViewer`, `GgazeGrid`, `Navigator`, `Loader`, `Thumbnail` | +| Functions | `module_action` (snake_case)| `navigator_new`, `navigator_next`, `viewer_set_texture`, `grid_get_selected`, `trash_bin`, `mover_move`, `opener_launch`, `runner_run`, `enhancer_apply`, `clipboard_copy_image` | +| Variables | `prefix_name` (type prefix) | `p_nav`, `p_texture`, `i_count`, `c_path`, `u_idx`, `b_wrapped` | +| Macros/constants | UPPER_SNAKE_CASE | `GGAZE_APP_ID`, `GGAZE_PREFETCH_N`, `NO_DEFAULT` | +| Enum values | `MODULE_PREFIX_NAME` | `GGAZE_SORT_NAME`, `GGAZE_SORT_TIME` | +| Callbacks | `name_cb` | `loader_ready_cb`, `navigator_changed_cb` | +| Static/private fns| `_prefix_name` | `_viewer_clamp_pan`, `_loader_pick_backend` | + +Variable prefixes: `p_` pointer, `i_` int, `c_` char/string, `u_` unsigned, +`b_` bool. Use them consistently. + +## Module layout (one module per file pair) + +Every module is a `foo.h` + `foo.c` pair named after its main type. Related +types (element, iterator, state) live in the same pair, not split out. + +``` +navigator.h / navigator.c → Navigator (+ NavigatorIterator if needed) +viewer.h / viewer.c → GgazeViewer +gridview.h / gridview.c → GgazeGrid +loader.h / loader.c → Loader (+ per-backend structs under loader/) +trash.h / trash.c → Trash +mover.h / mover.c → Mover (+ MoverDest) +opener.h / opener.c → Opener (+ OpenerProg) +runner.h / runner.c → Runner (+ RunnerScript) +enhancer.h / enhancer.c → Enhancer (+ EnhancerPreset) [GEGL, optional] +clipboard.h / clipboard.c → helpers (no type) — like settings +thumbnail.h / thumbnail.c → Thumbnail +settings.h / settings.c → wraps GSettings (no custom type, just helpers) +``` + +### Lifecycle: `_new` / `_delete` + +Every concrete type gets `Type *type_new(...)` and `void type_delete(Type *p)`. +Pair every `_new` with a `_delete`; never leave allocation unbalanced. + +```c +Navigator* +navigator_new(GFile *p_dir, GgazeSort sort); + +void +navigator_delete(Navigator *p_nav); +``` + +For types passed as `void*` to generic callbacks (e.g. `g_list_free_full`), +provide `void type_delete_cb(void *p_void)`. + +### Iterators + +When a module exposes traversal, follow +`TypeIterator *typeiterator_new(Container *p)`, +`void typeiterator_delete(TypeIterator *p)`, +`void *typeiterator_next(TypeIterator *p)`, +`_Bool typeiterator_has_next(TypeIterator *p)`. +The iterator type lives in the same file pair as its container. + +### Accessors + +`module_get_field(obj)`, `module_set_field(obj, val)` — macros for trivial +access, functions for non-trivial logic. Keep accessor macros side-effect-free. + +## Headers + +- **Header guards** UPPERCASE, derived from filename: + `#ifndef NAVIGATOR_H` / `#define NAVIGATOR_H`. +- **Header order** inside a header: guard, includes, macros, enums, structs, + `new`/`delete`, then the rest. +- **`.c` include order**: own header first, then system, then project headers. + Optional forward declarations between own header and the rest. + +## Errors + +- **Fatal** errors via the project's `ERROR(...)` (abort with a message). +- **Recoverable** errors via a documented return: a `RETCODE`, a `gboolean`, + or a `GError **` where GLib conventions apply (loader, file I/O). +- Decoders return `NULL` + set `GError **` on failure; the window shows a + placeholder and advances — never crash on a bad file. + +## Globals + +Minimal. Any global is named UPPER or `_prefix` and lives in the module that +owns it. No module reaches into another's globals — go through accessors. + +## GObject / GTK notes + +GTK/GObject types (`GgazeWindow`, `GgazeViewer`, `GgazeGrid`) use `G_DEFINE_TYPE` +and GObject conventions (constructed/dispose) for the GObject side; the +`_new`/`_delete` skill pattern still applies to the plain-C modules +(`Navigator`, `Loader`, `Trash`, `Mover`, `Opener`, `Runner`, `Enhancer`, `Thumbnail`) that have no GObject parent. +Keep the two worlds clean: plain-C modules own no GtkWidget and are unit-testable +without a display. + +## Testing + +Each plain-C module pair ships a `tests/test_.c` using GLib's `GTest` +framework (or a tiny harness), run via `meson test`. Aim for **≥80% line +coverage** (gcov/lcov: `meson setup -Db_coverage=true && meson test && ninja -C +build coverage`), gated in CI. GObject/GTK widgets get smoke tests only; the +logic lives in the plain-C modules so it's testable without a display. +Beyond tests, run the `auditing-code-quality` skill (C-adapted: +`c-best-practices` + `find-code-bugs` + `solid-principles` + +`beyond-solid-principles`) at milestone boundaries and track findings via +`agent-task-management`. \ No newline at end of file diff --git a/docs/gegl.md b/docs/gegl.md new file mode 100644 index 0000000..0fe54d5 --- /dev/null +++ b/docs/gegl.md @@ -0,0 +1,150 @@ +# GEGL Integration + +GEGL (Generic Graphics Library) — the data-flow, floating-point, +non-destructive image-processing framework behind GIMP, GNOME Photos, imgflo, +and iconographer. You chain *operations* (ops) into a graph; GEGL renders it. +Full op list: . + +ggaze uses GEGL for **quick, non-destructive photo enhance** plus a few bonus +capabilities. It is an **optional** dependency (meson `feature`); a minimal +build skips it and ggaze stays a plain fast viewer. + +## Scope stance + +ggaze is a *culling viewer*, not an editor. GEGL enhance is **opt-in and +non-destructive**: a preset is applied as a live preview to help judge a +keeper, and can be exported as a **copy** (the original file is never +modified). No layers, masks, undo stacks, or sidecar `.gegl` state. Full +editing remains a non-goal. + +## The quick-enhance feature + +- `a` → **enhance popup** (same popover pattern as `m`/`e`/`!`): lists presets, + each with an auto-assigned hotkey (`1`, `2`, … then `0`, `a`-`z`). +- Selecting a preset applies a **GEGL graph** to the current image and + re-renders the viewer through it — non-destructively. Press it again (or + `Esc`) to turn the preview off. +- `s` (or menu *Save enhanced copy…*) writes the enhanced result to a + new file, e.g. `IMG_0001-enhanced.jpg`, via a GEGL saver. Original untouched. + ggaze **never auto-saves** — the preview is a live overlay only. Moving to + another image (or quitting) with an un-exported preview prompts + Save/Discard/Cancel; `s` saves and clears dirty, `Esc`/re-press discards. +- Export format: defaults to the original extension (JPEG quality 95); a + format/quality chooser and a lossless `jpegtran`/`exiftool` path are later. +- Presets are configurable: `enhance-presets` GSettings `a(ss)` — ordered + `(name, gegl-graph)` pairs. Order = hotkey order. Ships with sensible + built-in defaults; user can add/edit in Preferences (`,`). +- GEGL runs **only** when a preset is active or on export. The default fast + decode path (GdkPixbuf / direct libs) is unchanged — the "fast" goal holds. +- Enhance is **not** applied during `h`/`l` scrubbing — only when settled on an + image — so flipping stays instant. + +### Built-in preset ideas (real GEGL ops) + +Defaults ship with the first ~12; the rest are optional/artistic. Each is a +one-shot GEGL graph applied as a non-destructive preview (`a` popup, `s` to +save a copy). + +| Preset | Graph (ops) | +|---------------|-------------------------------------------------------------| +| Auto-fix | `gegl:stretch-contrast` → `gegl:color-enhance` | +| Brightness | `gegl:exposure` (or `gegl:brightness-contrast` brightness) | +| Contrast | `gegl:brightness-contrast` (contrast +) | +| Saturation | `gegl:saturation` (or `gegl:color-enhance`) | +| Warm | `gegl:color-temperature` (warmer) | +| Cool | `gegl:color-temperature` (cooler) | +| White balance | `gegl:color-enhance` → `gegl:stretch-contrast` | +| Shadows | `gegl:shadows-highlights` (lift shadows) | +| Highlights | `gegl:shadows-highlights` (recover highlights) | +| Levels | `gegl:levels` | +| Curves | `gegl:contrast-curve` (gentle S-curve) / `gegl:curve` | +| Sharpen | `gegl:sharpen` (unsharp mask) | +| Denoise | `gegl:noise-reduction` (or `gegl:bilateral-filter`) | +| Clarity | `gegl:high-pass-filter` blend (local contrast) | +| Grayscale | `gegl:color-to-grayscale` (or `gegl:mono-mixer`) | +| Sepia | `gegl:sepia` | +| Vignette | `gegl:vignette` | +| Softglow | `gegl:softglow` | + +**Tunable parameters:** every preset is a `gegl-graph` string in +`enhance-presets` (`a(ss)`), so the exact strength (saturation amount, +contrast level, exposure stops) is editable in Preferences or via +`gsettings` — no slider UI needed. The Curves preset uses a fixed curve shape +(also editable in the graph text); a full interactive curve editor is out of +scope — hand off to GIMP (`e`) for that. A later "fine adjust" mode could +expose ± nudging of the active preset's main parameter. + +Graph strings are illustrative. Built-in presets are built programmatically +with `gegl_node_new_child`; user-authored presets can be stored as `gegl:gegl` +graph text and parsed with `gegl_node_new_from_xml`. + +## Crop, straighten & rotate tools + +Same non-destructive model as enhance (live preview graph + `s` to export a +copy), but interactive (crop/straighten) or one-shot (rotate): + +- **`c` — crop** (`gegl:crop`): adjustable rectangle; aspect-ratio presets; + mouse drag or keyboard (`h`/`l`/`j`/`k` move, `H`/`L`/`J`/`K` resize); `Enter` + apply. +- **`R` — straighten** (`gegl:rotate`): drag a horizon line or nudge the + angle (`h`/`l`, ±0.5°) with a grid overlay; optional auto-crop of rotated + corners; `Enter` apply. +- **`[` / `]` — rotate 90°** (`gegl:rotate-on-center`): one-shot CCW/CW; repeat + for 180°/270°. No overlay. +- Compose with enhance presets in the same graph. Large view only; GEGL + required. + +## Module + +`enhancer.{c,h}` → `Enhancer` (+ `EnhancerPreset`). Plain-C, no GtkWidget, +unit-testable. Runs GEGL in a `GTask` thread (off the UI thread). + +```c +EnhancerPreset *enhancer_get_presets(void); /* from GSettings */ +GeglBuffer *enhancer_apply(GeglBuffer *p_in, EnhancerPreset *p, GError **); +gboolean enhancer_export(GeglBuffer *p_in, EnhancerPreset *p, + GFile *p_out, GError **); +``` + +Viewer integration: when a preset is active, the decoded pixels are imported +into a `GeglBuffer` (GEGL GdkPixbuf-source op / babl), the enhancer processes +it, and the output buffer is rendered back to a `GdkTexture` for display. +This path is heavier, so it is strictly on-demand. + +## What else GEGL gives ggaze + +- **Color management** — `gegl:icc-file-loader`, `gegl:lcms-from-profile`, + `gegl:cast-color-space`, `gegl:convert-color-space`, plus ICC-aware savers. + Closes open question **G** (color management) via babl + LCMS, no separate + wiring. +- **Format load/save** — `gegl:jpg-load`/`-save`, `gegl:png-load`/`-save`, + `gegl:tiff-load`/`-save`, `gegl:webp-load`/`-save`, `gegl:ppm-*`, + `gegl:rgbe-*`, `gegl:gegl-buffer-load`/`-save`. Can augment GdkPixbuf on the + enhance/export path (JXL/AVIF/HEIF still need their own libs). +- **Thumbnail generation** — `gegl:load` → `gegl:scale-size` → save; an + alternative backend for the thumbnail cache. +- **Transforms** — **crop** (`gegl:crop`), **straighten** (`gegl:rotate`) and + **rotate 90°** (`gegl:rotate-on-center`) are in scope as tools + (`c`/`R`/`[`/`]`); lens correction (`gegl:lens-distortion`), red-eye + (`gegl:red-eye-removal`), and `gegl:scale-ratio` remain later/maybe. +- **Tone mapping** — `gegl:reinhard-2005`, `gegl:mantiuk-2006`, + `gegl:fattal-2002` (handy for linear/HDR-ish scenes). +- **Artistic** (optional/fun) — `gegl:vignette`, `gegl:sepia`, `gegl:softglow`, + `gegl:oilify`, `gegl:cartoon`, `gegl:photocopy`. +- **Batch** — drive the `gegl` CLI via the `!` runner for bulk enhance/export + without building it into ggaze's UI. + +## Costs & trade-offs + +- Heavier deps: `gegl`, `babl` (and transitively more). Gate behind a meson + `feature` so minimal builds and the core culling flow don't pay for it. +- GEGL processing is slower than straight decode — keep it off the hot path. +- `gegl-gtk` (GeglGtkView) is separate and thinly maintained — **avoid**; keep + the custom `GgazeViewer` widget and render GEGL output to `GdkTexture`. +- GEGL does **not** demosaic RAW — RAW stays out of scope. + +## Dependencies (Fedora) + +``` +gegl-devel babl-devel +``` \ No newline at end of file diff --git a/docs/goals-and-scope.md b/docs/goals-and-scope.md new file mode 100644 index 0000000..89af1c3 --- /dev/null +++ b/docs/goals-and-scope.md @@ -0,0 +1,102 @@ +# Goals & Scope + +## Goals + +1. **Fast.** Visible image on screen within ~100 ms of launch for a typical + camera JPEG. UI thread never blocks on decode. Flipping through a few + hundred shots in a session must feel frictionless. +2. **Native.** Looks and behaves like a GNOME/Fedora app: libadwaita styling + (decision pending), header bar, system themes, HiDPI, gestures. +3. **Keyboard and mouse, equally first-class.** Every GUI element has a + corresponding hotkey (shown on the element) or is reachable through a + keyboard-navigable menu; and conversely, every action that has a hotkey is + also reachable through the GUI (a header-bar button or a menu item). + Sensible vi-ish bindings so the `feh`/`nsxiv` audience feels at home. +4. **Two views, one window.** A thumbnail grid for overview and a large + single-picture view for detail. Flip between them instantly; both are + keyboard-driven. Minimal chrome in either. +5. **Directory-aware.** Given one file, navigate its siblings without a separate + browse step. Prev/next, wrap, default sort by filename (EXIF capture-time as + an option), filter by extension, **hide RAW sidecars by default** (toggle to + reveal). The grid shows the whole folder at a glance. +6. **Culling-friendly deletion.** `d` bins the current picture into a local + `./Trash` folder (recoverable, lives with the shoot); `D` deletes it + permanently. The grid dim-marks trashed items so you can see progress. +7. **Triage by moving.** Mark any number of pictures and send them to a + configured destination folder with `m` → a quick popup whose entries each + carry an auto-assigned hotkey. Destinations are user-configurable (an + ordered list of name → path pairs). +8. **Modern formats.** PNG, JPEG, GIF, WebP, plus JPEG XL / AVIF / HEIF + (pluggable loaders). +9. **Good citizen.** `.desktop` file, MIME handler for `image/*`, AppStream + metadata, Fedora RPM packaging, man page. +10. **Self-documenting UI.** The interface explains itself: hotkeys printed on + their elements, tooltips on every control, a `?` shortcuts overlay, + badges/counters/toasts that narrate state, and an info overlay for EXIF. + A user should not need the manual to use it. +11. **Hand off to other tools.** `e` opens the current image in a configurable + external program (GIMP, identify, another viewer, …) via a popup with + auto-assigned hotkeys. ggaze stays open and responsive while the external + tool runs. +12. **Run shell scripts.** `!` runs a configurable shell script (e.g. + `~/scripts/usbimport`) asynchronously through `/bin/sh -c`, with `%f`/`%d` + placeholders, and rescans the folder on completion. +13. **Quick non-destructive enhance.** `a` applies a configurable GEGL preset + (auto-fix, brightness, contrast, saturation, sharpen, denoise, …) as a + live preview; `s` exports an enhanced **copy**. ggaze **never auto-saves** — navigating away from an un-exported + preview prompts Save/Discard/Cancel. The original is never modified. GEGL + is optional; the core viewer stays fast without it. +14. **Copy to clipboard.** `Ctrl+c` copies the current image (pixels, PNG) — + or, with marks, the marked files (URIs) — to the clipboard, so it can be + pasted into other apps (Katogram, GIMP, file managers) like gthumb. +15. **Crop, straighten & rotate.** `c`, `R`, and `[`/`]` are non-destructive + GEGL tools for cropping, leveling the horizon, and 90° rotation; `s` + exports the result, the original is never modified. GEGL is optional. +16. **Compare before/after.** Hold `Space` to flash the original image; + release to see the modified preview — to judge whether to `s` save. Large + view; needs an active preview. +17. **Correct orientation.** Honor EXIF Orientation on load so portrait and + tilted camera shots display upright automatically; manual rotate/straighten + compose on top. +18. **Open anything.** Accept a file **or a folder** as the argument + (`ggaze ~/Downloads/Camera/` opens the grid), and accept drag-and-drop of a + file or folder onto the window to open it. +19. **Live folder monitoring.** Watch the current directory with `GFileMonitor` + so external additions/deletions (e.g. `usbimport` writing files) show up + without a manual reload. +://20. **Tested.** ≥80% unit-test coverage on the plain-C modules (navigator, + detect, thumbnail, mover, opener, runner, enhancer, trash, settings), + measured via gcov/lcov with a coverage gate so it doesn't regress. +21. **Audited quality.** Run the `auditing-code-quality` skill (adapted for C: + `c-best-practices` + `find-code-bugs` + `solid-principles` + + `beyond-solid-principles`, findings tracked via `agent-task-management`) + at each milestone boundary and before release; fix all HIGH/MEDIUM findings. + +## Non-goals (at least initially) + +- **Full image editing** (layers, masks, crop-save, annotate). Quick + non-destructive enhance + export-copy via GEGL is in scope (goal 13); deep + editing is not. +- **RAW development.** May show embedded JPEG preview later, but no demosaic. + GEGL does not demosaic either. +- **Library / catalog / albums / tags.** That is beets-for-photos, not this. +- **Cloud / network sources.** Local files only. +- **Batch processing in-app.** Bulk convert/resize is a job for the `!` runner + (`gegl` CLI) or external tools, not ggaze's UI. +- **GEGL as a hard dependency.** It stays an optional feature; a minimal build + is a plain fast viewer. + +## Target user & workflow + +Someone who just downloaded a shoot from their camera into a folder and wants +to cull it: flip through fast (bursts produce many near-identical frames), +glance at EXIF (shutter/aperture/ISO/timestamp) to pick the sharpest or best-timed +shot, trash the rejects, and move on to editing the keepers elsewhere. + +Typical session: `ggaze ~/Downloads/Camera/IMG_0001.jpg` opens the folder as +a thumbnail grid, `Enter` jumps into the large view, hold `l` (or `→`) to scrub forward, +`d` to bin obvious misses into `./Trash` (or `D` to delete outright), `v` to +mark keepers, `m` then `1` to ship them to "irregular ninja", `i` when a +frame is borderline, `e` to open a keeper in GIMP, `Esc` back to the +grid, `q` to leave. No library, no +database, no sidecar state — just the folder, faster. \ No newline at end of file diff --git a/docs/open-questions.md b/docs/open-questions.md new file mode 100644 index 0000000..e6962f6 --- /dev/null +++ b/docs/open-questions.md @@ -0,0 +1,238 @@ +# Open Questions + +Decisions to make before/while coding. None block M0. + +Most items are now **decided** — see PLAN.md decisions #29–#38. Each item's +**Decision:** line records the chosen value; the milestone-level Leans are +locked in decision #37. + +## A. libadwaita or plain GTK4? +- libadwaita → GNOME-native styling, follows release style, but opinionated + (header bar patterns, no theming overrides). +- plain GTK4 → full control, works under any GTK theme, less "GNOME-native". +- **Decision:** libadwaita (decision #29). + +## B. App ID / GApplication name +- Placeholder `org.buetow.ggaze`. Confirm domain + naming. +- **Decision:** `org.buetow.ggaze` (decision #30). + +## C. Image decode strategy +- Rely on GdkPixbuf loaders for the common case, or link libjpeg-turbo / + libpng directly from the start for the progressive low-res preview feature? +- **Lean:** GdkPixbuf first; add direct libs in M5/M6. + +## D. Thumbnail grid view — in scope (decided) +- Two views per window: a thumbnail grid (overview) and a large single-image + view. `Enter`/`Esc` switch between them; cursor stays in sync. +- Trashed/deleted items stay listed but dimmed (with a hide toggle). +- **Lean:** grid view is a core feature, M7. Not "a different app" after all. + +## E. Single instance? +- `GApplication` single-instance means a second `ggaze img.jpg` reuses the + window and loads the new image. Convenient; small risk of state confusion. +- **Decision:** single instance, replace on new `open` (decision #32). + +## F. Recursive directory walking +- `ggaze folder/` → recurse into subdirs, or just flat siblings? +- **Lean:** flat by default; add `--recursive` later. + +## G. Color management +- Ignore sRGB-only for now; plan a later milestone for lcms2 + ICC. +- Confirm target displays are sRGB-ish (most are). +- **Update:** if GEGL is enabled (M9), color-managed decode/export comes via + babl + LCMS (`gegl:icc-file-loader`, `gegl:lcms-from-profile`, + `gegl:convert-color-space`) — no separate lcms2 wiring. Without GEGL, stay + sRGB-only. + +## H. Scroll behavior default +- Scroll = zoom (feh-style `--scale-zoom`) vs scroll = next/prev. +- **Lean:** zoom; expose in settings since it divides users. + +## I. Trash vs delete (decided) +- `d` moves the file to a local `/.Trash/` directory (created lazily, + collision-suffixed), undoable with `u`. This is **not** the system trash. +- `D` permanently deletes (unlink), no undo. +- **Lean:** confirmed; `./Trash` lives with the shoot for easy inspection/emptying. + +## J. Packaging targets +- RPM first (Fedora native). Flatpak too? Copr repo? +- **Lean:** RPM + AppStream; Flatpak later. + +## K. Camera-dump specifics +- **RAW + JPEG pairs.** Many cameras shoot both. Show only the JPEG and hide + the matching `.RAF/.CR3/.NEF`? Or show RAW via embedded preview? Hide-pairs + is the culling-friendly choice. +- **Burst grouping.** Auto-detect burst sequences (EXIF burst ID or + sub-second capture time clustering) and collapse them into a group, letting + `j`/`k` step group-by-group with an expand for within-burst? Big UX win for + the stated use case, but scope creep — decide whether it's M-something or + "later". +- **Default sort.** Capture time (EXIF `DateTimeOriginal`) vs filename. Camera + dumps are usually already named in shot order, so filename sort ≈ capture + order; but capture-time sort is more robust after a burst of edits. +- **Import folder.** Any integration with the camera-download location, or just + "whatever path you point it at"? Lean: just a path. +- **Decision:** burst grouping deferred to "later"; hide RAW sidecars by + default (toggle to reveal); default sort = filename (capture-time as a menu + option); import folder = just a path (decision #33). + +## L. Custom viewer widget vs GtkPicture +- `GtkPicture` is simplest but limited zoom/pan. A custom `GtkWidget` gives + cursor-centered zoom, pan clamping, animated transitions. +- **Decision:** custom viewer widget (decision #31). + +## M. Destination configuration storage +- GSettings `a(ss)` (ordered name→path) is simplest and integrates with + `gsettings`/dconf. Alternative: a small TOML/ini under + `~/.config/ggaze/destinations.conf` for hand editing. +- **Lean:** GSettings `a(ss)` via the Preferences dialog; revisit if users want + to hand-edit. + +## N. Move vs copy, and collision policy +- `g_file_move` (rename / copy+delete) by default; a copy mode ("send a copy + without removing") could be a modifier (`M` for copy?). +- Collision: suffix `-1`, `-2`, … (never overwrite). +- **Lean:** move + suffix; copy as a later option. + +## O. Auto-hotkey scheme past 10 destinations +- `1`-`9`, `0`, then `a`-`z` gives 36 slots — plenty. Beyond that, a scrollable + list navigable by arrows + `Enter` (no single-key shortcut). +- **Lean:** digits then letters; cap at a sane number. + +## P. Undo depth +- One level (`u` undoes the last `d` or `m`) is enough to start; a small stack + (10) is a cheap upgrade later. +- **Lean:** one level now, stack later. + +## Q. Marks vs re-sort / view switch / trash +- Marks are path-based and held by the navigator: a re-sort reflows but keeps + marks; a view switch preserves them; trashing a marked item clears its mark. +- **Lean:** confirm during implementation. + +## R. External programs — command format & launch +- Store `name → command` pairs; command uses freedesktop `Exec` placeholders + (`%f` single file, `%F` multiple). Start with `%f` only; `%F` (marked set) + later. +- Launch via `GSubprocess` detached (non-blocking), or `g_app_info`? + GSubprocess is simpler for raw commands; `g_app_info` is better if reusing + `.desktop` entries. +- **Lean:** raw command + `%f` + GSubprocess; revisit `.desktop` reuse later. +- Should `e` apply to marks (`%F`)? **Lean:** current image first; marks later. + +## S. Shell scripts — execution model +- Run via `/bin/sh -c ""` (POSIX sh) or `bash -c`? Use the user's + `$SHELL`? **Lean:** `/bin/sh -c` for portability; revisit for bash-isms. +- Async, non-blocking; toast while running and on completion. On exit, + rescan the directory (scripts may add/remove/move files). A destructive + script could rename the current file away — rescan handles it (cursor falls + back to nearest). +- Placeholders: `%f` (current image), `%d` (current folder); `%F` (marked + set) later. **Shell injection:** single-quote substituted paths (filenames + can contain spaces, `$`, backticks) — or pass paths via env/argv instead of + `sh -c`. +- **Lean:** `/bin/sh -c`, single-quote paths, rescan on exit, `%f`/`%d` now. + +## T. Thumbnail size — range, step, and cache +- freedesktop TMS defines only 128 (normal) and 256 (large) cached sizes. + For larger grid thumbnails, decode a custom size on demand (downscale the + full image or the `large` cache entry), cached in a separate bucket. Range + ~64–512px, step ~32px. Confirm. +- Persist `thumbnail-size` (int) in GSettings; restore on launch. +- **Lean:** 64–512px, ±32px step, custom cache bucket for non-TMS sizes. + +## U. GEGL integration +- **Hard or optional dep?** Lean: optional meson `feature`; core viewer works + without it. Confirm RPM splits a `ggaze-gegl` subpackage or keeps it in. +- **Preset format:** store `gegl-graph` text (parsed via `gegl:gegl` / + `gegl_node_new_from_xml`) or build programmatically per known preset? Lean: + built-in presets built programmatically; user presets as graph text. +- **Apply timing:** only when paused on an image (not during `h`/`l` scrub); + disable the preset during rapid navigation, reapply on settle. Confirm. +- **Viewer path:** import decoded pixels → GeglBuffer (GdkPixbuf-source op or + babl), process, render output buffer → GdkTexture. Measure overhead; maybe + show a subtle "enhanced" badge in the header. +- **Export naming:** `-enhanced.`; collision → suffix `-1`. Same dir + as original by default; configurable later. +- **gegl-gtk?** No — keep the custom viewer, render to GdkTexture. +- **Save flow:** `s` = save enhanced copy; no auto-save; navigate-away (or + quit, or `d`/`D`/`m`) with a dirty preview prompts Save/Discard/Cancel. + Slideshow moved to `S` to free `s` for save. Confirm the prompt also fires + on `d`/`D`/`m` of a dirty image (Lean: yes). +- **Decision:** optional m