diff options
Diffstat (limited to 'docs/architecture.md')
| -rw-r--r-- | docs/architecture.md | 242 |
1 files changed, 242 insertions, 0 deletions
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 |
