1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
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`.
|