# 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`.