diff options
| author | Paul Buetow <paul@buetow.org> | 2026-07-12 17:00:43 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-07-12 17:00:43 +0300 |
| commit | 78fa24a665002c01f0ea7ded8791c7e6af470305 (patch) | |
| tree | aaa5625337ff4cc97f40f855405c9091b3b3496a /docs | |
Add ggaze implementation plan (docs/)
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/PLAN.md | 230 | ||||
| -rw-r--r-- | docs/README.md | 55 | ||||
| -rw-r--r-- | docs/architecture.md | 242 | ||||
| -rw-r--r-- | docs/coding-conventions.md | 127 | ||||
| -rw-r--r-- | docs/gegl.md | 150 | ||||
| -rw-r--r-- | docs/goals-and-scope.md | 102 | ||||
| -rw-r--r-- | docs/open-questions.md | 238 | ||||
| -rw-r--r-- | docs/roadmap.md | 120 | ||||
| -rw-r--r-- | docs/tech-stack.md | 109 | ||||
| -rw-r--r-- | docs/ui-and-interactions.md | 390 |
10 files changed, 1763 insertions, 0 deletions
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 `<folder>/.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 `<name>-enhanced.<ext>`; 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 `<stem>-enhanced.<ext>` 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 `<dir>/.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 → <dir>/.Trash/<name> (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 "<cmd>" + → 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.<ext> 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_<module>.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, +a |
