Skip to content

Latest commit

 

History

History
1035 lines (889 loc) · 97.1 KB

File metadata and controls

1035 lines (889 loc) · 97.1 KB

PicFetch — Architecture

Navigation map for AI agents. PicFetch is a Fyne desktop image viewer: one binary, split into internal/... packages. Start here to find a file. Standing rules (data flow, concurrency, conventions, build) live in AGENTS.md — do not duplicate them here.

Package map

github.com/frathe/picfetch (package main)

Entry point only. main.go parses the command line (launchArgs, see internal/launch) before any side effect, dispatches the private similarity.WorkerMain subprocess mode before desktop startup, calls openwith.Install (first statement after that, see internal/openwith), skips GitHub-update predecessor cleanup for Store-managed builds and explicit Explorer trials, asks launch.Options.ApplicationID to validate and select the app identity before building the fyne.App, loads embedded translations/*.json, converts CLI paths to URIs (argsToURIs), and calls ui.Run. main_darwin_test.go asserts the graft landed — this is the only test binary that links the Cocoa driver.

scripts/historymovie

Development-only Git history films, reached through make movie and make movie-test. run.sh captures one HEAD and its NUL-delimited log/tree snapshots, builds the Docker tool image, and owns container cleanup. movie.py replays ancestor changes with merge-tree reconciliation, checks the final file set, and generates the Gource animation, titles, live counters/chart, original score and verified MP4. The renderer receives exported history and an output directory, with networking disabled; it does not mount the repository. Dockerfile defines the tools and test_movie.py guards replay and timing.

scripts/testshards

Repository tooling for measured Linux race-test sharding. It parses complete go test -json streams, emits deterministic package/top-level-test summaries, plans a reviewable manifest from median durations with stable LPT tie-breaking, checks that manifest against the build-selected Linux test inventory, derives the fail-closed non-UI package partition, and emits exact shard filters. Its capture path streams compact partition/package/test diagnostics while retaining the raw events outside the repository; the generated assignment lives at .github/testshards/internal-ui.tsv.

File Responsibility
main.go summarize, plan, check, regex, partition, and capture command paths: complete event-stream validation, deterministic assignment, live build-selected inventory and parallel-call checks, exact anchored filters, exact-package subtraction, concise diagnostics, and raw preservation.
docker-race.sh Public Make runner: unique host artifact directory, attached Docker lifecycle, raw streams and console retention, exit/memory/OOM diagnostics before cleanup.
main_test.go Command-boundary fixtures for event streams and capture, deterministic planning, every manifest rejection, build-selected runnable forms, parallel-call refusal, exact filter generation, package partitioning, and Make contract expansion.

internal/similarity

Local content analysis shared by the viewer and its reproducible experiment. client.go owns Client.Analyze (selected source paths, control channel, immutable Event callbacks), asset discovery, the cancellable analysis subprocess, and private WorkerMain dispatch. Each request captures the caller's encoded-file size limit; the worker installs it before source reads. Explorer requests also capture configurable item and serialized-map memory limits, with item admission checked by both client and worker. analysis_protocol.go streams snapshot metadata, individual items and hierarchy merges, enforcing the same aggregate memory budget at both ends and publishing only a completed snapshot to the caller. Resource errors survive subprocess cleanup for the settings-guidance toast. analyze.go accounts for every input, captures source versions, reuses canonical full oriented decoding, makes previews, and publishes a map on manual request, optionally every 30 sources, and at completion. facts.go captures versioned oriented dimensions, normalized extension and optional EXIF camera/calendar-date facts from already-read sources; old cached representations backfill these facts without inference. tags.go exposes catalogue identity for rule compatibility. Event.Measurements carries cumulative worker stage times, inference attempts and publication counts as immutable values; grouping includes its named sub-stages. control_unix.go/control_other.go own the pollable worker input descriptor. worker_linux.go installs worker-only seccomp denial on x64/ARM64 synchronized across all threads before reading requests; worker_other.go retains the macOS sandbox launcher. worker_windows.go launches a hidden ordinary subprocess with closeable control pipes; Windows does not install OS network denial and events keep OfflineVerified false. offline.go exposes that distinction through EnforcesNetworkIsolation and assembles the Linux filter for host-independent BPF decision tests. cache_favorites.go owns the shared Favorite inventory, retained directory handles, list versions and producer lease. Both producer admission and maintenance use it; unknown membership remains inspectable without admitting unleased writes. cache.go persists successful favorite representations in analysis beside file-list.json/thumbs, validates source/model/preprocessing versions, and uses directory handles plus file-list identity to avoid recreating removed favorites. cache_payload.go reads one JSON document through a hard byte bound, then checks its version, source identity shape, vector, digest and preview. General/Favorite reads, general write validation and maintenance share that decoder. Explorer's analyzer and search share the representation store. When the general cache is enabled, both reuse and write compatible loose-image records; Explorer also promotes compatible hits into newly saved Favorites. Explorer sends Favorite membership even when its analysis preference is off, with a separate opt-out that blocks both Favorite records and general fallback for those members. Producer inventory preserves healthy Favorites alongside per-entry errors; incomplete membership disables general reuse/writes for that producer. Cache maintenance retains each Favorite directory and list version through inventory and rechecks membership before stale removal. Inventory handles unreadable Favorite definitions independently so healthy peers remain inspectable and cleanable. encoder.go (cgo) sets the non-Windows telemetry opt-out before library loading, disables telemetry through the runtime API before session creation, and owns the pinned native SigLIP 2 session; encoder_nocgo.go keeps cross-platform package builds available and reports that inference requires cgo. tags.go decodes the generated embedded tag-vectors.bin, validates its canonical float32 digest against tag-catalog.json, and applies those text prototypes to fresh and reused image vectors against 75 fixed subjects, without a text runtime; scripts/tagvectors converts the retained JSON source before Make builds; grouping.go owns independent 15D grouping and 2D layout fits plus canonical cohort identities; hierarchy.go orders a centroid spanning tree in grouping space for local granularity cuts. Repeated source paths share one assignment. Display events retain this compact hierarchy and omit inference vectors, which remain in the worker and favorite cache. The encoder and grouping entry points are worker-only: the native runtime is process-global and batch algorithms cannot be interrupted in place. assets.go/assets.sha256 verify pinned local assets; assets_install.go checks local availability and performs explicit, cancellable HTTPS installation into a per-user cache. It bounds and hashes downloads, extracts only named runtime/license files from tarballs or Windows x64/ARM64 ZIPs and publishes verified files; Windows ZIPs include both pinned runtime DLLs and skip debug symbols. Store builds verify/load DLLs beside the executable, ignore model-cache runtime overrides, and download only model data. assets_package.go verifies architecture-specific archives for MSIX staging and extracts both DLLs and the upstream license, third-party notices and privacy document. Asset availability is separate from analysis admission; analysis never starts a download. offline.go verifies actual TCP/UDP OS denial on macOS/Linux; files.go registers the driverless read-only file repository. Production setup/analysis supports Intel/Apple Silicon macOS, glibc Linux x64/ARM64 and Windows x64/ARM64; native library evidence collection remains macOS-only. platform_darwin_amd64.go checks the native macOS product version before Intel analysis/setup admission (13.4 minimum); platform_other.go leaves the other platforms' existing admission unchanged. Unknown Intel OS versions fail closed.

Search uses search_rank.go for bounded exact cosine top-k and search.go for the retained, reference-first worker session with progress and 100-source result publications. search_client.go owns query/event IPC and observes writer and subprocess completion; search_worker.go uses canonical decoding and compatible Favorite representations, validates source versions before publication, and retains vectors rather than previews between queries. A committed Favorite save carries an acknowledged cache revision on the coalesced query lane. The retained worker refreshes ownership and persists prepared members, regenerating previews without inference; cache opt-outs still refresh ownership without writing Favorite records. Transient search progress is limited to one update per 100 ms, while ranked, failure and terminal events retain exact accounting. The first general-cache capacity refusal reduces that producer's write scope to Favorites only, preserving reads and in-memory preparation. Explicit Favorite refresh carries that scope forward; new producers receive fresh admission. Search reports pressure at session readiness so automatic eviction cannot interrupt its preparation, including when a reference failed or was abandoned. search_pipeline_test.go exercises retained queries through real cached source preparation and complete-scope version validation. Its warm benchmark includes store reopening, cache reads, validation and progressive ranking; the existing session benchmark supplies prepared vectors directly. Both exclude native model startup, subprocess transport and UI paint.

internal/ort

Narrow build-time selection of the ONNX Runtime Go binding used by similarity's encoder and scripts/explorertags. binding.go uses upstream v1.36.0/API 29; binding_darwin_amd64.go uses unmodified v1.25.0/API 23 through the explicit internal/ortlegacy module alias in go.mod. Intel macOS pins Microsoft's last official runtime 1.23.2 (macOS 13.4+); other platforms retain 1.29.0. Exactly one binding may enter each binary because both wrappers define the same C symbols. binding_test.go guards all six platform/architecture selections. Both files require cgo; similarity retains its existing no-cgo encoder.

internal/hdbscan

The independently MIT-licensed HDBSCAN subset from PhotoPrism, pinned with its copyright/license and source provenance in LICENSE and README.md. It uses only the standard library. hdbscan.go builds and condenses the mutual-reachability hierarchy; labels.go owns core distances and worker iteration; union_find.go tracks merge ancestry; common.go retains only required validation, distance and error declarations. Similarity grouping calls it synchronously with one worker, minimum cluster size four and minPts three (self plus two other neighbors). Its adapter retains a single root cohort when no smaller cluster qualifies.

internal/explorerpresets

Reusable local preset definitions, stable identities and versioned rule matching over immutable similarity facts. presets.go owns the rule contract and atomic, cancellable global library storage; root composition supplies its directory. The Explorer feature runs I/O on its separate preset workers and shared UI queue.

internal/explorertrial

session.go owns an explicit native trial's exclusive evidence directory, source-free causal records and final collection summary. Numbered analyses bind input digests to received events, actual UI application and observed worker exit. Session-unique event IDs link receipt to application and foreground-surface observations; grid-result/current-image digests distinguish frozen browsing from the updating map without retaining paths. Foreground MapView records pile count, zoom/floor, camera center and viewport; map departure/return binds the camera to its applied revision even when grouping changes behind a cohort. It also records cohort/map visits, rejected truncated scans, and shutdown. Identity gives trial launches separate Fyne preferences/session storage. Collection has no implicit human quality verdict; UI application is not paint. The module starts no goroutines.

scripts/explorereval

Bounded local experiment reached through make explorer-setup, make explorer-test and make explorer-evaluate. main.go owns the experiment CLI/offline launch and the explicit -install entry point; make explorer-setup calls it directly without requiring Bash, and setup.sh remains a compatibility wrapper for the same verified Client.InstallAssets used by the viewer. make explorer-download-test qualifies download/reuse without loading native code. Platform runtime archives are selected in internal/similarity/assets.go and extracted library hashes remain in internal/similarity/assets.sha256. evaluate.go uses the shared native encoder/grouping and adds per-stage measurement and evidence files; files.go selects the bounded smoke corpus. report.go and review.html produce local cohort/measurement artifacts; memory_*.go measures native RSS; evaluate.sh retains each run/exit status. TRIAL=search adds a local exact-cosine relevance experiment. search.go validates the bounded reference/judgment manifest; search_evaluate.go owns canonical decoding, version-checked preparation, ranking and measurements. search_report.go/search_review.html retain a local interactive relevance report with judgment export; search_cache.go runs a separate production Favorite cold/warm baseline against the captured corpus. These experimental rankings and timings do not establish a human usefulness verdict or production search UI behavior. Real-model tests require assets under the explorertrial tag and check the platform's actual network policy; Windows must not claim OS denial. make explorer-ui-test additionally exercises the production worker and completed-map/Grid View round trip through the UI harness. profile.go, reached with make explorer-profile, runs bounded cold/warm passes through the actual similarity.Client with a temporary favorite cache, retains aggregate event timings and a source-identity digest, then removes that cache. Its completed summary observes both worker exits; no previews or source paths are written to the profiling report. native_darwin.go/native_other.go implement TRIAL=library: retain a native app bundle and executable digest, launch under OS denial with its own process group, forward cancellation for graceful shutdown then escalate only that group, observe child/descendant exit, and sample aggregate process-group RSS once per second. The normal app scans the supplied library without the smoke sampler. Launcher logs, exit status and collection/qualification fields remain separate from the app's numbered analysis records. It never scans source files outside denial.

scripts/explorertags

Development-only regeneration of the embedded semantic tag vectors. prepare_tokens.py uses the pinned Gemma tokenizer for fixed catalogue prompts; main.go verifies the pinned text tower and runs it with the existing native ONNX Runtime, producing readable normalized float32 JSON keyed by tag identity. The viewer requires neither this text model nor Python. See README.md for reproducible commands/provenance.

scripts/tagvectors

Offline build-time conversion of authoritative tag-vector JSON into exact little-endian float32 data. main.go validates identities, dimensions, norms and the catalogue digest; -check guards generated-file freshness. Make builds regenerate internal/similarity/tag-vectors.bin, which NewTagger embeds and validates. The JSON and generated binary are both retained in the repository.

scripts/appassets

Development-only derivation of embedded illustrations and compact gaze atlases from retained originals. main.go owns source/output paths and display pixel targets; -check compares dimensions and decoded pixels without an encoder. Generation uses local cwebp; the application keeps its existing image decoders.

Packaging tooling

scripts/updaternotices reconciles the six-target production updater dependency union with its reviewed manifest.json, checks source-file hashes and generates the bounded updater section of THIRD-PARTY-NOTICES.md. artifacts.go verifies byte-identical license/privacy/notices inside finished ZIP/tar.gz archives and both MSIX bundle payloads; CI runs this before release publication/Store upload.

packaging/tools.mk owns reviewed CLI versions and multiarchitecture image digests consumed by Makefile and the release/Store workflows. docs/packaging-inputs.md describes provenance and native artifact validation. scripts/plistdoctypes derives macOS file associations from supported formats; scripts/msixstage stages Store manifests/assets and guards packaging routes.

scripts/storepublish

Microsoft Store release command: record binds the WACK-validated x64/ARM64 bundle to its tag and producing run; prepare freezes a GitHub-only approval manifest; preview and check read release/Store data; submit and reconcile consume that exact manifest to drive one approved release lifecycle. release freezes the prior reconciliation and current submission under one approval, advancing only after the exact prior receipt is confirmed published; standalone operations keep their existing scope. release.go admits exact artifacts, notes.go generates bounded plain-text notes and preserves listing metadata, github.go discovers immutable build artifacts and journals receipts in deployment payloads, microsoft.go owns OAuth/submission/blob requests, and lifecycle.go resumes recorded operations. approval.go binds review to artifact/notes/base/receipt identity; environment-policy.jq checks the saved main-only required-reviewer policy. main.go/contracts.go provide command parsing and per-invocation dependencies; main_test.go exercises the command against persistent fake services. .github/workflows/microsoft-store-publish.yml pins trusted main across preparation and reviewer-gated jobs after Store builds or manual dispatch. Certification observation and recovery are manually dispatched; reconciliation never admits a new release. docs/microsoft-store.md covers setup and recovery.

scripts/nativeguards

Native Windows/macOS and explicit Microsoft Store validation. main.go selects suites, verifies build-selected inventories, runs the full packages, retains raw Go test events and requires named guards to run/pass without skips. The macOS suite includes root main's Cocoa-linked delegate test; Windows includes native Unicode transport, wallpaper and updater guards. CI invokes this command and uploads its event files. main_test.go covers admission, selection, event validation, process failures and workflow wiring through a per-call runner.

internal/ui/explorer

Feature owns the analysis/setup/editing workflow around the existing Map. feature.go defines its injected runtime options, value observations, settings, cohort snapshots, trial observations and close/stop/wait/settle contract. workflow.go captures prepared source paths and Favorite/cache ownership, controls native analysis, loads Favorite cohorts and rejects stale UI delivery. lifecycle.go owns feature-local request tokens. setup.go owns first-use acceptance, local asset checks and explicit cancellable download/retry. cohort_workflow.go owns captured Unassigned-selection review and rollback; preset_workflow.go/preset_fields.go own preset dialogs, matching and saves. Analysis/setup/cohort I/O and preset work have separate tracked worker groups and one drainable UI queue. Settle joins/drains/repeats and reports delivery so root can repeat any resulting Grid work; SettlePresets observes preset completion while analysis streams. Close cancels without joining on UI; Stop ends admission and Wait joins workers off UI.

The consumer-side WorkflowHost carries window/input access, separate menu and repaint notifications, cohort/exit/return transitions and presentation facts for trial recording. Repaint is distinct from menu notification because source reconciliation may still be updating Grid indexes when analysis retires. Root's explorerInput retains duplicate preparation, selected Favorite identity, trial launch admission and maximize consumption. No worker or workflow field is exposed to the viewer. feature_test.go, setup_test.go and cohort_workflow_test.go exercise the module without the full viewer harness; root tests retain cross-feature transitions and native trial integration.

presets.go captures frozen candidate facts and applies reviewed preset membership, protecting other groups and pending members. cohorts.go finds shared recognized tags in selected Unassigned sources, matches chosen traits, and captures named current-map cohorts by source identity. Manual assignments overlay immutable analysis results, survive publications and stay separate from granularity cuts. Snapshot/restore preserves explicit favorite members across partial analyses. Creation reveals the new cohort and rejects stale/non-Unassigned sources.

tags.go owns the localized checkbox catalogue, clickable unique-source counts opening captured tag-only grids, OR filtering of intact piles/Unassigned, and cohort subject titles from up to two tags covering at least half the distinct sources; explicit cohort names take priority. The toolbar collapses/restores the sidebar. Choices survive map publications and cohort visits; full exit clears them. Filtering starts no background work.

Map owns the clipped pan/zoom surface, opaque toolbar, Unassigned entry and stable samples of up to fifteen distinct members per Pile, fitted thin frames, Shift-scroll panning and a granularity slider cutting the supplied hierarchy. Slider release rearranges piles and centers the selected source or arrangement at the current zoom; background publications retain shared-source anchors. Map.View captures source-free camera geometry on UI for explicit trial recording. keys.go owns keyboard zoom, directional stack selection, source-bound selection restoration and camera reveal; Enter opens the highlighted stack. layout.go matches continuing cohorts by shared sources, orients the initial projection to the window, and places new piles with a minimum gap using nearby-cell collision queries and perimeter searches. Its narrow Host opens the full captured cohort, leaves the map, sends manual/automatic update controls, and supplies current input modifiers. The Map itself starts no workers; Feature delivers partial/final results and optionally expands the camera for new stacks. Root maximizes the native window at entry and coordinates Grid/image transitions; Feature retains the camera and frozen cohort across them. internal/ui/grid/subset.go filters cohorts by source path while retaining root indexes for existing operations. Replaced pile images have their sources cleared and refreshed to release Fyne renderer/texture references. Leaving Explorer clears the map; cohort round trips retain it. map.go retains compressed sample sources for the complete map, decodes piles within one pile-width beyond the viewport, and releases decoded pixels/textures beyond two pile-widths. Distant pile renderers expose no child objects, preventing Fyne's minimum-size walk from decoding them. Returning piles restore their pixels before paint; selection redraws reuse unchanged previews.

Committed writes through filework.go and removals through viewer.RemoveFile retire the analysis/map while preserving surviving cohort identities; load.go keeps missing-file retries within the remaining cohort.

internal/ui/visualsearch

Reference-driven browsing over a captured original source scope. feature.go owns immutable origin/current visits, the latest 20 successful reference visits, Back/Exit and branching; session.go owns a retained native provider, queued session/query/revision delivery, and observable suspension and shutdown. Root supplies source identity, Grid presentation and origin restoration through a narrow Host. The feature's Settle drains finite query delivery without waiting for an idle retained worker; Stop followed by Settle joins retired producers. internal/ui/visualsearch.go composes command admission, shared Explorer setup, ranked Grid visits and complete presentation/restoration deliveries. Deferred Back carries its saved Grid state until the surface can accept it; application reads current preparation progress from the live search session. Capacity pressure requests maintenance once at readiness, after the current Favorite-save revision is acknowledged. The prepared worker then remains available for reference changes under the existing revoked-lease protocol. Image origins and Grid bookmarks use internal/fileidentity occurrence values when merge mode repeats a source. Grid retains one index per collection generation; image-origin lookup captures only the bookmarked path. An image opened before the first publication retains its Grid anchor until the first successful visit commits. Terminal search failures revalidate the captured collection through root's tracked file-work lane before restoring the origin; request, session and collection identities reject obsolete reconciliation. internal/ui/sourcechange.go owns admitted source changes: search detaches its origin before callbacks, then root reconciles the complete removal/write/recovery, retires Explorer, updates Grid and restores the origin. Display failures receive the selected origin index through their existing retry chain instead of starting a competing load. Committed writes retain comparison for Grid origins and close it when an image origin must be restored. internal/ui/browsing.go captures the shared ranked/cohort command restriction and one immutable ranked index order per action or preload pair; menus and duplicate handlers consume the same subset restriction. Favorite capture exposes one CurrentFiles snapshot to the naming/overwrite workflow. internal/ui/searchoverlay.go observes dismissal of generic canvas overlays while a result is pending, with one acknowledged UI callback and cancellable, tracked worker completion through shutdown and the test harness.

internal/ui/grid/ranked.go owns ordered path-based Grid visits, captured selection/filter/highlight/scroll state (including duplicate occurrence ordinals), determinate progress and deferred revision installation during marquee gestures. Rank revisions preserve the collection's stable host indexes. The reference leads up to 30 other matches and has a purple outline below ordinary selection/highlight feedback. Completion hides the progress bar while retaining the result and counts. Favorites AddFiles captures its list before naming and overwrite dialogs begin. Ranked filtering and visit capture/restoration share a generation-bound source index, so repeated result updates inspect only their bounded ranked identities. The worker retains its top-k between preparation batches; a changed reference resets ranking to one full prepared-scope pass.

internal/ui/analysiscache

Owns the Settings Cache tab and serialized inspection, retuning, cleanup and automatic eviction. feature.go presents measured general/Favorite usage and partial reports. operation.go owns explicit intent admission, coalesced usage refresh/reserve requests, provider dispatch and accepted policy effects. Refreshes wait behind mutations; automatic eviction and policy retirement survive view close. Persistence toggles commit independently of inspection success, retire producers on UI and join their barriers even after Settings closes. work.go captures providers/roots, joins predecessors, dispatches maintenance and suppresses retired view callbacks. quiescence.go owns the UI handoff after shared write-lease revocation: cancellation can abandon a queued callback, but a claimed callback's reply and all retired producer barriers belong to completion. Host reasons distinguish local policy retirement before inspection, explicit record removal after lease revocation and automatic eviction. Cache inspection holds only one queued progress callback, reading the latest count on delivery, so inventory size cannot create an unbounded UI progress backlog. Root composes the two-method Host in internal/ui/analysiscache.go, uses the Fyne application cache root, persists accepted policy and disables new analysis admission while maintenance owns the roots. Automatic eviction notifies search through CacheWritesRevoked, permitting an idle, fully prepared producer to retain its vectors after lease invalidation; pending preparation/Favorite writes still join. The disk manager retains its existing lock/epoch protocol and final committed-effect report. Settings supplies the tab slot, confirmation window and Close notification without sharing worker state.

internal/similarity/cache_store.go owns Favorite-first/general record reuse and producer write scope: Explorer and search both begin with every enabled store; capacity pressure can narrow an individual producer to Favorite-only writes. Explorer reports that pressure only with its completed map, so root can run the existing automatic eviction after preparation. Neither producer reaches into the store to route a write. cache_payload.go validates shared representations; cache_records.go confines inventory to managed records and temporary files. cache_management.go reports usage and implements conservative stale cleanup/general LRU eviction. Canceled maintenance retains its measured remainder after a complete inventory under the lease; an interrupted inventory reports its partial counts as incomplete. cache_transaction.go owns observations, committed removal accounting and final outcomes on every return path. Reconciliation respects cancellation. Missing source paths remain unavailable without persisted volume identity, even when their parent directory exists. A successful general-only retune persists despite incomplete Favorite usage. cache_lease.go plus build-tagged cache_lock_* provide root epochs and native advisory locks shared by Explorer/search subprocess writers and maintenance. Old writers cannot publish after a cleanup; installed model/runtime assets are outside these roots. The harness drains both new per-instance UI queues and production shutdown joins both features after stopping admission.

internal/ui

The application. Unexported appState is the file-set model (scan/drop order, displayed order, index, sort, merge) and publishes an immutable dupes.Snapshot — file keys plus generation — atomically on every write to files; viewer.Generation() reads the generation out of that snapshot rather than a separate counter. Unexported viewer is the Fyne façade. Construction order, overlay order, data flow, and concurrency: see AGENTS.md. Features expose state; internal/ui composes them.

The concurrency invariant: see AGENTS.md § Concurrency and Fyne.

Its own files

File(s) Responsibility
run.go Run: restore startup viewer, start runtime (favstore.DefaultDir, position polling), register shutdown and CLI drop, enter the Fyne loop. Shutdown retires title/menu updates, cancels feature work and flushes preferences without rebuilding retired native menus. Store-managed builds skip GitHub update startup and staged-binary apply. Explicit trial startup reserves new evidence, isolates Favorites/presets/updates, disables update activity, auto-opens Explorer after the ordinary scan, and joins its signal watcher and workers before finalizing evidence.
build.go buildViewer composes widgets and registerFeatures modules and snapshots distribution.StoreManaged onto the viewer. Overlay tail: copy selection, similarity map, grid, comparison (including its pointer shield), delete confirm, export prompt, toast. Desktop canvases also receive the chained comparison key-down hook for exact physical Ctrl+L; ordinary typed-key and shortcut wiring remains separate.
startup.go loadStartupState / restoreStartupGeometry / buildStartupViewer — the one load→build→restore path shared by Run and tests.
components.go Dropzone, scan, sort, and info-overlay constructors. Toast stays in toast.go.
trane.go Welcome-screen Trane: hosts widgets.Gaze with a compact 17-cell atlas. Owns pointer/window-layout coordinates, scaled dead zone, immutable decode cache and magenta-spill correction within five source pixels of transparency. Hide/MouseOut forget pointer position and circle progress. A hover-only surface preserves input across the restore link; ten circles request Help.ShowFinis. No timers or background workers. scripts/appassets retains the used pixels from assets/trane/codex-pet/spritesheet.webp.
explorer.go Root adapter for Explorer: captures duplicate-prepared sources, composes setup acknowledgment with preferences, maps frozen cohort identities to collection indexes, and coordinates map/Grid/image transitions. The feature owns workflow, dialogs, workers and delivery. explorerInput retains collection/launch/window policy only.
features.go registerFeatures assigns help, EXIF, zoom, copy selection, grid, similarity map, comparison, mosaic window, deletion, slideshow, settings, then favorites.
shortcuts.go wireGlobalShortcuts plus per-action shortcut wiring (open, favorites, clipboard, copy selection, comparison, delete, select-all, save, export, wallpaper). Comparison registers the native Cmd/Ctrl+D plus physical Ctrl+D when those differ. yieldingShortcuts blocks ordinary commands during comparison and otherwise yields Copy Selection; Open is admitted only far enough to show comparison's refusal. Copy Selection and clipboard bindings also defend their own direct entries.
gesture.go Position-poller callback fans samples to winPos and spiralDrag; a recognised spiral calls the viewer's openSpiralForGesture.
tunnel.go Both Spiral trigger doors; freezes the loaded main-order URI list through one installed duplicate-visibility snapshot, without preparing groups or narrowing to grid selection.
windowtrack.go Main-window size tracker and position poller; widgetGeometry / prefGeometry translate preferences.WindowGeometrywidgets.Geometry.
windowmenu.go Window-menu action handlers (showViewer, showWindowExif, showWindowGrid, showWindowPictureFrame, showWindowHelp — grid/picture-frame mutual exclusion lives in the first two) plus refreshMainMenu / syncNativeMenuBar and the Darwin sync entry points. The Checked/Disabled matrix itself lives in internal/ui/menus.
windowmenu_darwin.go After Show and every native rebuild, fold Window items into GLFW’s NSApp.windowsMenu and clear AppKit’s default Command mask on unmodified letter accelerators.
windowmenu_notdarwin.go No-op twin of the Darwin native-menu merge.
testdata/ Golden screenshots for the e2e suite.
state.go Unexported appState. Only viewer accesses it.
sourcechange.go Complete source-removal, committed-write, validation-recovery and analysis-policy transitions. Detaches search before callback delivery and restores browsing after collection/cohort/Grid reconciliation; display keeps retry ownership.
lifecycle.go requestLifecycle / requestToken for root scan/sort/copy-selection and other root work. Display owns its load/GIF/SVG lifecycles internally.
viewer.go Façade: title (baseTitle / gridTitle / comparison ownership / applyTitle), reset/close (clearToDropzone releases cached and recycled-cell images through grid.InvalidateContent), merge, Host vocabulary (CurrentFile, ShowImage, RemoveFiles, …).
visibility.go dupeFileSet (adapts the viewer to dupes.FileSet by forwarding appState's published dupes.Snapshot); jumpIfHiddenExtra; pushHideDuplicates; the navigation helpers (nextVisibleIndex / firstVisibleIndex / lastVisibleIndex / randomVisibleOther) that read v.dupes instead of polling the grid overlay.
keys.go handleKeyEvent / handleTypedRune, plus a chained desktop key-down hook that requests the ready-gated comparison link toggle on exact physical Ctrl+L without key-repeat flapping. Return immediately while Canvas().Overlays().Top() is set (Fyne dialogs/menus). Comparison owns all main-window typing: Escape closes it, F1 opens Help, 0 / 1 / + / - reach its shared camera or hovered photo pose, swipe-mode Left / Right / Home / End reach its divider, and every other key/rune stops before the still-open grid. Copy Selection: HandleKey consumes Escape/copy/navigation; unowned keys yieldCopySelection except modifier-only and zoom keys.
menu.go buildMainMenu builds internal/ui/menus.Menus and assembles the bar: File, Favorites, Actions, Window, Help. yieldingMenuCallbacks enforces comparison isolation and Copy Selection yielding at callback entry. menuState() is the one function that builds the menus.State snapshot; syncMenus() applies it, pushes comparison/file availability into Favorites, and refreshes the native bar only when something actually changed.
actionmenu.go Comparison-guarded Actions-menu handlers (setActionsSort, toggleActionsHideDuplicates, showActionsVariant, rotateActionsImage, …). The Checked/Disabled matrix lives in internal/ui/menus.
drop.go handleDrop / applyScanResult / applyScannedFiles glue over filescan.Images / filescan.Siblings; scan lifecycle is viewer.scanOp. A non-empty drop is refused before any state change while comparison is active.
openwith.go macOS "Open With" delivery: installOpenWithHandler / openInitialFiles / openFilesFromOS over internal/openwith, both routed through fyne.Do so a launch carrying argv files and a delivery makes one handleDrop. The combined pending set is cleared before that shared path refuses an active comparison, so deliveries cannot queue behind it.
memlimits.go settings value, settingsState / ApplySettings, memory-limit get/set that retune caches and imaging.SetMaxEncodedBytes.
theme.go Settings-facing appearance getter/setter; applies internal/appearance modes live.
favthumbs.go Viewer glue for favthumbs.Sync and gridSink; captures the saved Favorite preview limit (default 1000) and retires active work on edits; per-request completion plus all-pass worker tracking and terminal shutdown cancellation.
load.go ShowImage admission and display callbacks: requested/probed/presented/failure, cohort retry and neighbor choices; autoResizeToImage / resizeToImage / syncWindowToZoom (static-size gate).
toast.go Self-dismissing notification card and ShowToast.
info.go Persistent info overlay (I); EXIF link; RAW (preview) mark.
asyncop.go asyncOpUI (lifecycle, active, done, spinner) — used only by scan and sort.
sort.go toggleSort / SetSortMode / startSort / finishSort over filesort.Order; lifecycle is viewer.sortOp.
rotate.go Rotation commands compose display intents with zoom/window/menu policy. displayedDimensions reads the display snapshot's oriented logical size.
vector.go Forwards zoom scale and canvas pixel conversion to display; SVG work and policy live in display/vector.go.
save.go, filework.go File > Save Changes captures pixels and source, owns cancellable workers and causal UI delivery, then reconciles the current resolved source. Committed stale Save/Export/Strip/mosaic writes invalidate cache revisions, thumbnails and duplicate facts; tracked background identity/info reads preserve unrelated views and retry after intervening commits.
export.go File > Export image (promptExport / exportAs / runExport) via widgets.ChoiceCard + filepicker.ChooseSave. Carries an exportRequest (format plus imaging.ExportOptions) and reports the applied size limit in the suggested name and the toast.
exportoptions.go The export prompt's extra rows (exportOptions, a widgets.ExtraRows): the export size limit rungs and the "Include camera metadata" widget.Check, reset to defaults on every open. Both Fyne controls grab canvas focus on tap; the checkbox hands it back from its own OnChanged (every effective tap toggles it), while the rungs are tappable labels because a radio item focuses without firing OnChanged when the value doesn't change.
mosaic.go Cross-feature mosaic composition: snapshot the loaded collection, or explicit Grid selection/complete filtered result while Grid View is open; inspect displays, open the singleton workflow, and adapt generation/display refresh into its narrow Host.
wallpaper.go Shared ordinary/mosaic wallpaper lifecycle: serialize work, export captured pixels to a global or hashed-target cache scope, call wallpaper.Set(Request), and sweep only copies no longer backing another scope.
autoupdate.go Viewer-side update glue: maybeStartUpdateCheck gates the opt-in daily check; CheckForUpdatesNow adapts manual worker callbacks through fyne.Do with an inner staleness check; PerformUpdate records relaunch intent and requests quit; maybeShowWhatsNew consumes the cached update marker and opens this build's bundled release notes. Every update entry point refuses a Store-managed viewer. Policy, staging, and cache live in internal/ui/autoupdate.
slideshow.go togglePictureFrameMode (closes grid first) plus shuffle/interval bindings.
batch.go Routes delete/copy commands by the current subject: image-region selection, grid selection, or displayed image.
session.go restoreSession glue over internal/session.
clipboard.go Copy-path / copy-image glue over internal/clipboard; captures a displayed image and encodes PNG off UI.
clipboardwork.go Per-viewer clipboard admission, cancellation, encoder and result queue; whole-image/grid workers finish before queued delivery, while the operation Signal includes its UI effects. Shared admission preserves region-copy routing and its existing completion contract.
reveal.go Actions > "Reveal in file manager" (Cmd/Ctrl+R) and the info overlay's link of the same name: current-file path, own goroutine behind viewer.reveal, toast on failure. Glue over internal/filemanager.
copyselection.go Viewer adapter for internal/ui/copyselection: availability, start/cancel, zoom Geometry to View, animation pause, clipboard worker, yieldCopySelection. Command entry yields through yieldingMenuCallbacks, yieldingShortcuts, handleKeyEvent, and handleDrop.
compare.go Viewer adapter for internal/ui/compare: validates exactly two explicit grid selections, resolves ascending host indices to URIs, unfocuses the covered grid so desktop modifier hooks remain reachable, and loads through the canonical full-image cache/probe/decode path. That path preserves EXIF-corrected pixels, RAW previews, animation decoding/budget policy, encoded-input limits, and the original first frame; the feature deliberately freezes animation. The adapter owns the exact comparison-window title callback and reports failures without mutating the grid or file set. comparisonActive() is the composition layer's sole exclusive-mode fact; refuseOpenDuringComparison() owns the localized discard policy.
internal/ui/display/animation.go, pause.go Own acknowledged GIF playback and acquisition-bound stable captures. The root composes Copy Selection and observes display completion.
openfiles.go UI-owned native open admission. Each request captures the native chooser, tracks its worker and queues results through private chooserUIQueue. New requests, accepted drops, reset and shutdown invalidate old delivery; comparison is rechecked on UI. closeOpenChooser stops admission without waiting for a blocking native panel. Harness settlement waits all native workers, then drains results before scan/sort/load waits.

Feature packages (internal/ui/...)

Package Responsibility Reaches back via
internal/ui/zoom/ Zoom/pan of the displayed image. Geometry / HandleScroll / SetOnGeometryChanged are the presentation seam Copy Selection uses; this package does not import copyselection. Window growth is syncWindowToZoom in internal/ui. onChanged, modifiers, onScaleChanged.
internal/ui/copyselection/ Transient Copy Selection mode: image-region geometry, overlay, and captured Source crop/encode. HandleKey reports whether the mode consumed the key. Copy, Ended, Scroll.
internal/ui/grid/ Overview (G): GridWrap, thumb cache, decodepool, uiqueue.go, work.go (cancellable sessions and terminal Stop), search, badges, explicit host-index selection plus its change observer, marquee.go (drag rectangle → Targets()), browse-duplicates (Shift+D), and hashengine.go's per-session hashing pass that feeds internal/dupes. Each work session owns one decode queue: current Grid cells enter newest-first high priority, while duplicate hashing enters low priority; cancellation resolves queued callbacks without waiting for occupied source reads. Closing cancels reads, reopening resumes unfinished hide analysis, and revision-tagged cell claims isolate old completions. Captured FactWriters reject obsolete hashes, failures and sizes; groupwork.go coalesces cancellable grouping on one independently tracked worker so partial hiding progresses while source reads occupy the decode pool; it admits only current snapshots; inspect retarget waits for accepted groups. Shutdown Stop is terminal; Settle waits the decode pool and grouping worker, drains queued completions, and repeats. nav.go: setHighlightHighlightChanged; scrollfollow.go follows wheel/trackpad movement with the ring and reconciles live filter reflows within the viewport. Reads the model; does not own it. 10-method Host including Modifiers.
internal/ui/compare/ Opaque main-window comparison surface: switchable gapless 50/50 and full-viewport swipe layouts compose two persistent photo transforms with one shared camera transform; each image has one reveal clip so swipe keeps aligned image coordinates. In Swipe, each pane input mirrors its current reveal even though the render viewport remains full-size, and reveal-local wheel coordinates are translated back into that viewport. The ready-gated top-left Unlink/Link control and physical Ctrl+L share ToggleLink; its adjacent status reports only the active unlinked target, while layout/Swap/Back stay in a separate top-right card. ToggleLink changes only input ownership and never changes rendered geometry. Linked pan/zoom moves the camera, linked 0 frames both current photo poses without rewriting them, and linked 1 returns the camera home. Unlinked pointer input and transform keys target the hovered or last-hovered photo; its 0 / 1 fit or show that photo at decoded-pixel size in the current camera. Photo centers and camera movement stop when an image edge reaches its pane center. Resize and layout preserve both photo poses and the camera; Swap deliberately clears divergence from the last-targeted visible pose before exchanging sources. A private paneRenderer scene seam keeps transforms independent from presentation; production owns two stable canvas.Shader objects while tests can inject the canvas reference adapter. Each immutable render source retains the canonical decoded frame, a long-edge-1024 overview, and a 64 MiB detail-tile cache. The planner uses physical display density and the actual side-by-side/swipe reveal, skips details when the overview is sufficient, and binds at most seven guttered detail tiles without shuffling stable sampler slots. One cancellable worker per pane generates tiles; publications are coalesced and marshalled through the feature's UIQueue. Pan/zoom changes shader geometry and uniforms without repainting the viewer root. Each SVG still gets a pane-local device-pixel raster, clamped by imaging.ClampVectorRaster, before entering the same overview/tile path. Settle covers load, vector, tile, and causal queued completions with reusable channel-epoch barriers. Fyne's software test painter does not render canvas.Shader, so deterministic pixel tests use the reference adapter; native runtime acceptance uses the GL painter. The feature also owns divider input, permanent chrome, ready-gated layout/link/Swap controls, the input shield, and the replaceable completion signal. It receives an ordered URI pair and never reads or mutates grid/viewer state. Loader plus Callbacks (Repaint, Closed, Failed, OrderChanged, Modifiers).
internal/ui/mosaicwin/ Dedicated configuration/preview window with immutable command-entry sources/topology, accessible controls and focus order, distinct labels for identically named displays while preserving target IDs, cancellable generation lifecycle, progress.go coalesced coverage/live-canvas updates on the tracked worker/UIQueue, a determinate coverage bar, stale-result rejection, exact-result export, targeted wallpaper callback, and remembered geometry. Partial previews preserve a separate finished image for cancellation/failure; export actions stay disabled during generation and Cancel remains in the visible surface. It is a secondary window, not a main-window overlay. 4-method Host (GenerateMosaic, InspectMosaicDisplays, SetMosaicWallpaper, AfterFileExported).
internal/ui/deletion/ Shift+Delete confirm (widgets.ChoiceCard) then trash.Move. RequestFiles snapshots unique URI targets; successful moves reconcile by identity on UI across reorder/replacement. uiqueue.go owns completion dispatch; Close stops admission/unstarted moves and suppresses late callbacks, and Settle drains test completions. 6-method Host, including ReconcileDeletedFiles.
internal/ui/slideshow/ Picture-frame mode (P): full-screen, auto-advance, interval, winpos.Tracker capture/restore. uiqueue.go owns delayed advance dispatch; the worker waits for a buffered application acknowledgement, and Exit/Close cancel waits independently of UI. Kick discards an already queued timed advance. Close stops shutdown work without geometry restoration; Settle waits workers before draining stale test callbacks. 2-method Host. Knows nothing about the grid.
internal/ui/exifwin/ EXIF panel (E): metadata.go owns cancellable source reads, generation-checked tag/GPS/action presentation and content-based removal inspection/status and separate MetadataDone completion. stripwork.go owns cancellable removal, busy admission and a committed WriteResult in the Host notification. GPS map (tiles.go, tilework.go, startWarm): four shared workers, a 64-job queue, 256 expiring failure entries and a 16 MiB encoded-byte cache. Navigation/close cancels old reads and tile sessions; collapse cancels tiles. uiqueue.go owns result delivery and Settle waits/drains removal, metadata, warm and tile workers repeatedly. Shutdown Stop is terminal. Geometry via widgets.Singleton. 4-method Host.
internal/ui/help/ Manual, About, release notes/What's New (whatsnew.go), Help menu; embeds manual.md / manual_de.md and this build's release-notes.md. make release copies the canonical .github/release-notes.md into the bundle alongside the version bump. Both notes entry points share the bundled file and a singleton with a GitHub release-history link. releaseart.go replaces Markdown images before layout and loads GitHub-hosted HTTPS URLs on up to three background workers; releaseimage.go owns the allowlisted redirect policy and bounded HTTP fetch/decode. releasework.go cancels on close, stops admission on shutdown and exposes Wait/Settle with a per-instance UIQueue. Notes without images start no workers. Secret search phrase calls the viewer’s registered SetOnSpiral callback; finis in manual search opens the cursor-following companion (finis.go, embedded finis.webp), hosting widgets.Gaze with centered portrait geometry and its own hover surface. ShowFinis also serves welcome Trane; ten independent circles reveal the localized, wrapped bubble in finis_clue.go, whose click opens an empty focused manual search. New(app, title, art) plus optional event callbacks.
internal/ui/spiral/ Full-screen shader easter egg. tunnel.go owns serial preview admission and three texture slots; playback.go advances bounded GIF frames on the existing UI clock with independent flight origins; flow.go owns cycles, batch variation and route selection; flight.go owns safe route geometry used for admission/retirement; shader.go renders depth, feather and translucent composition. uiqueue.go marshals preview/frame callbacks with session checks. H toggles local help; F1 invokes the viewer's manual callback. The shared centre stays within a resized viewport. Viewer supplies a frozen URI value to Show / ShowForGesture; Close cancels on UI and test Settle joins/drains off UI. Process shutdown does not join uninterruptible preview source reads.
internal/ui/settingswin/ Settings: General/Appearance/Updates/Limits/Cache, update dialogs, snapshot seed, live apply, Singleton geometry. Show(State, storeManaged) replaces the GitHub update controls with Store-owned-update copy when applicable. Show(State, bool) + Host (ApplySettings, CheckForUpdatesNow, PerformUpdate).
internal/ui/favorites/ Favorites menu and add/overwrite/manage/remove dialogs. New does no disk I/O; SetDir from Run. SetCommandsEnabled preserves Add's file availability while disabling both static and dynamically rebuilt menu entries during comparison. 6-method Host.
internal/ui/menus/ The stateful File/Window/Actions menu items and their whole Checked/Disabled matrix as Apply(State) (changed bool), a pure function of a value snapshot. ComparisonActive applies a final all-ordinary-items-disabled override while leaving Help available. Fyne-typed but viewer-free, unit-testable with no app. Menu-bar assembly, the Darwin native-bar fold, the real shortcut bindings, and every action the items run all stay in internal/ui. No Host: Apply(State) over a value snapshot built by menu.go's menuState().
internal/ui/autoupdate/ Shared serialized automatic/manual update worker: lazy verifier/client preparation, check/download progress events, same-process matching-stage reuse, all-worker settle, last-check-day persistence, staged apply/relaunch intent, the What's-New cache (whatsnew.go), and the apply-failure cache (applyfailure.go) — ApplyStagedUpdate writes it when update.Apply fails, and internal/ui reads and clears it on the next launch. Persisted stages carry a process-ephemeral authentication seal, so a stage from an earlier run is redownloaded and re-attested rather than trusted from the user-writable cache. Both UI caches are one JSON document each in app.Cache(), over the saveCacheJSON / loadCacheJSON / clearCacheJSON helpers in cache.go; a failed relaunch is deliberately not recorded, since it happens after the new binary is installed and verified. No Host: takes a context.Context and a staleness func per call (Start / StartManual), plus Persist and per-Updater verifier-factory seams — cancellation stays the viewer's own requestLifecycle, not promoted here.
internal/ui/infoview/ The persistent info overlay (I key): its four widgets - text, the EXIF link, the reveal link, the card - the current file's raw facts (byte size, EXIF presence, RAW-preview flag), its own toggle preference, and formatFileSize. The EXIF link follows HasEXIF; the reveal link is shown with the card itself. No Host: Update(State) / Sync(bool, State) over a value snapshot built by info.go's infoState().
internal/ui/display/ Single-image surface publication, rotation/fades, source-bound observations and action captures. feature.go owns the surface, snapshots and worker settlement, capture.go saved baselines and stable captures, animation.go/pause.go playback and its acquisition gate, lifecycle.go private request tokens, vector.go sharpening, load.go complete navigation/retries/handoff and preload.go bounded speculation. One UI queue delivers workers; separate completion observations distinguish loaded/applied/stopped. Feature; root shares Surface() with zoom for geometry only.
internal/ui/widgets/ Shared UI mechanics: ChoicePanel / ChoiceCard (+ its optional ExtraRows slot above the button row, Up/Down between them, Return offered to the focused row before it commits, and SetSelectionActive muting the button ring so only one mark is ever at full strength), TappableArea, Singleton (+ geometry memory), NewSizeTracker, focus-ring style. gaze.go extracts the compact single-row atlas and owns the 16-direction/neutral portrait presenter shared by Trane and Finis; callers own artwork preparation, hosting and face-relative coordinates. circlegesture.go recognizes timestamped head-relative pointer turns; hosts own independent instances, geometry normalization and lifecycle reset. Leaf aside from internal/winpos.
internal/ui/assets/ Embedded viewer artwork, including ExplorerIntroPNG for first use. Leaf.

internal/imaging

Viewer-independent probe → decode → EXIF-orient → cache pipeline (JPEG, PNG, GIF including animated, WebP, BMP, TIFF, ICO, XPM, AVIF, SVG, camera RAW via embedded JPEG). RAW is preview-only (LoadedImage.Preview); CanEncode is false. SVG is the only vector format (svg.go / vector.go). Encode/write-back for a subset of formats lives in save.go; mutations.go serializes resolved destinations. Thumbnail caches carry favthumbs.Preview source versions for safe reuse after file replacement, and ByteCache.Capture binds in-flight cache writes to the pre-purge revision.

File Responsibility
bytecache.go ByteCache[V]: goroutine-safe LRU by estimated bytes. Add admits foreground images even over budget; generation-bound CacheWriter.AddIfRoom admits display preloads only into remaining space without eviction or promotion. RefreshIfRoom lets Favorite warming replace stale keys, dropping only that key if the replacement cannot fit. AddIfFits keeps its existing individual-size gate and may evict. LoadedImage.DecodedBytes shares retained pixel/vector accounting with the mosaic repeat cache.
loader.go LoadedImage, NewImgCache, ReadAndProbe, CaptureDateContext (cancellable metadata reads), DecodeLoaded (pixels), DecodeRecord (complete full-cache facts), LoadImage, IsSupportedImage, SupportedExtensions, MaxEncodedBytes / InputTooLargeError.
ico.go Explicit ICO probe/decode dispatch, independent of the desktop driver's decoder registration: validates directory/payload spans and dimensions, selects the same single image for probe/decode, delegates PNG or normalized uncompressed DIB pixels to existing decoders, and applies icon transparency.
raw.go Largest embedded JPEG from TIFF IFDs or SOI scan (CR3/RAF).
svg.go SVG detection, logical-size floor (MinVectorWidth/Height = UI startW/startH), ClampVectorRaster / MaxVectorRasterPixels.
vector.go Vector / ParseVector / ParseVectorContext / RasterAt.
svg_limits.go SVG preflight limits encoded bytes, XML depth and conservative expanded work. Allows direct definition reuse, rejects use inside definitions, and estimates expanded source storage for the cache.
exif.go Orientation tags from JPEG APP1, PNG eXIf, WebP EXIF, and TIFF IFD0 + ReadMetadata / Metadata (including GPS IFD). Metadata scans JPEG APP1, then TIFF IFD0, then ISOBMFF through the AVIF metadata parser, then RAW preview APP1.
exififd.go Unexported IFD walker (walkIFD) and tag value helpers used by exif.go and raw.go.
exifformat.go Unexported display formatters for exposure, focal length, and Exif dates (formatExposureTime / formatFocalLength / formatExifDate / parseExifDateTime).
orientation.go ApplyOrientation, RotateSteps.
resample.go Rolling-row CatmullRom downscaling for YCbCr sources in ScaleForExport; retains sixteen-bit source colors and float64 intermediates with bounded row storage.
gif.go Cancellable animated GIF decoding/compositing, probeGIF, shared frame-count and memory admission (palettes, source/output pixels, frame objects and scratch), and logical-canvas restoration for a frozen partial first frame without decoding later frames.
thumbnail.go LoadThumbnail / LoadThumbnailAndBounds and their context-bearing forms / NewThumbCache: same probe+decode, then downsample; LoadThumbnailAtEdgeContext supplies bounded static previews at a caller-selected ceiling; LoadThumbnailAndBounds also returns native ReadAndProbe size for hide-duplicates. Also FitEdge (the shared longest-edge rule) and ScaleForExport (CatmullRom, for exports) beside the unexported ApproxBiLinear scaleToFit thumbnails use.
preview.go LoadAnimatedPreviewContext shares GIF compositing and static thumbnail decoding. Chooses a smaller edge to fit retained animation bytes, bounds native decode pixels/frame count before decode, and falls back to the static first frame when those limits are exceeded. Spiral uses a 512px ceiling and 16MiB per preview.
dhash.go DifferenceHash / Hamming for grid hide-duplicates.
jpegseg.go Unexported JPEG header-segment walker (walkJPEGSegments) used by exif.go and jpegexif.go, plus jpegFrameSize reading the SOF frame size the file's own dimension tags describe. Stops at SOS; does not walk entropy-coded scans (jpegLength in raw.go) or metadata removal (jpegprivacy.go).
jpegexif.go Unexported metadata-preserving JPEG segment copying for save.go, plus the in-place TIFF patcher: orientation normalization, next-IFD unlinking, and the dimension-tag correction Save Changes and export apply across IFD0, the Exif SubIFD and the Interop IFD (reached through 0xA005) once the written frame stops matching them - patchIFDDimension rewrites what it can hold honestly, removeIFDEntries takes the rest plus the two coordinate tags no size can repair. Both refuse an IFD that does not wholly fit.
jpegprivacy.go InspectJPEGMetadata, its clean/removable/unsupported result, complete marker/scan removal policy, bounded decoding, JFIF/Adobe reconstruction shared by inspection and the serialized lossless mutation.
jpegremovalexif.go Bounded EXIF rendering-declaration validation and reconstruction: orientation, chroma positioning and enumerated color declarations only, with fresh directories and no source payload copying.
jpegmemory.go Header-only admission for the metadata-removal working-memory budget: encoded copies, padded/subsampled component planes, progressive coefficients and validation decoding; shared cancellable marker-fill traversal for admission and complete removal parsing.
jpegstructure.go Private qualification of 8-bit Huffman sequential/progressive frames, tables and complete component/coefficient scan progression.
jpegentropy.go Removal-only Huffman syntax validation: exact scan/restart consumption, one-bit padding, MCU traversal and bounded progressive nonzero masks; pixel reconstruction remains with the standard decoder.
jpegicc.go Bounded ICC chunk assembly and reconstruction of qualified RGB matrix/TRC and gray/TRC profiles; neutral descriptions and header identity, validated numerical transforms, no copied gaps/private tags.
grouping.go Cancellable greedy complete-linkage grouping; equal hashes reuse their assignment, and narrow distances use verified 16-bit projection candidates. Membership/order remain unchanged; internal/dupes chooses native-pixel representatives.
mutations.go Live file-identity transactions shared by Save/Strip/Export and external WithFileMutation participants such as Trash; case aliases share admission across atomic replacements, with cancellable admission/I/O and WriteResult commit identity.
save.go SaveRotated, Export (+ ExportOptions: size limit, metadata omission and exact-path fallback encoder), CanEncode / CanEncodeExt, StripJPEGMetadata. dimensionTagsInvalidated decides whether the source's dimension tags still describe what is being written, by comparing the written bounds against the source's own frame header - so a resize, a viewer rotation and an Orientation 5-8 source all correct them; Save Changes and export share that policy, retaining tags when geometry is unchanged or the source frame cannot be read (subject to export's resize fallback).

internal/avifpolicy

An imaging compile dependency with files only under nodynamic && !wasm2go. Unsupported tag combinations cannot build the viewer or imaging-dependent tools. Make, CI, packaging and nested test inventory/execution supply no_emoji,nodynamic; the pinned AVIF dependency therefore selects embedded WASM/wazero and excludes its automatic native library loader. imaging/avif_policy_test.go checks the dependency's file selection on all six shipped platform/architecture targets.

internal/favstore

Named file lists under a caller-supplied config directory. No UI. Saved path metadata uses private directories/files. DefaultDir is the production path helper and creates an isolated temporary fallback when user configuration is unavailable.

File Responsibility
favstore.go Save / Load / Count / DefaultDir; trash-backed remove.
cohorts.go Favorite-owned named source memberships, preset links and explicit Unassigned overrides in version-2 cohorts.json; legacy arrays migrate on the next save; cancellable atomic writes bound to the observed file-list identity, with removed members filtered on load.

internal/favthumbs

Disk-cached grid previews under <favorite>/thumbs/. Sync is the background pass; Sink is the caller’s in-memory thumb cache.

File Responsibility
store.go / name.go On-disk lookup and filename scheme; ReadContext / WriteContext check cancellation through cache decode/encode and before atomic replacement.
sync.go Cancellable Sync walk: memory → disk → decode, then Sink; bounds original-decode preparation to the configured number of unique source paths (default 1000) and serializes those decodes. Later entries reuse memory/disk previews without original reads. A completed pass prunes against the full Favorite membership.
sweep.go Deletes stale preview files; Sync's membership scan and deletion walk check cancellation, preserving current tail previews and the offline-source guard.

internal/session

Last-open file set via Fyne’s app-scoped cache.

File Responsibility
session.go Save, Load.

internal/update

GitHub-release check, SHA-256 + immutable release attestation verify, stage, apply.

File Responsibility
update.go Client, AssetName, Newer, Due.
github.go Releases + release-attestation HTTP.
checksums.go VerifyHash (optional API digest).
download.go / extract.go Fetch with optional DownloadProgress, hash and attest the archive, extract directly from verified memory bytes, and select payload paths/hashes from the archive-entry manifest. Persist Stage provenance under a process-local HMAC seal; restart requires redownload.
attest.go GitHub Fulcio Sigstore Verifier + in-toto release policy.
tufroot.go Offline 60-day expiry check and verified sync of embed/tuf-repo.github.com/root.json.
apply.go / apply_unix.go / apply_windows.go Apply dispatcher with ApplyOptions, normal shutdown without relaunch, explicit Perform-update relaunch. Unix (apply_unix.go) writes <dest>.new beside the target, renames it into place, and rolls back through <dest>.old on failure. Windows (apply_windows.go) replaces the running executable in-process from a retained file handle that denies concurrent writes/deletes and whose bytes matched the authenticated stage digest, then verifies the installed destination against that digest; it never reopens the mutable cache path after validation. It used to run a generated <dest>.apply.cmd through cmd.exe, which Controlled Folder Access refuses outright regardless of cmd.exe's own Microsoft signature; that script is gone.
swap.go swapBinaryFrom: the Windows in-process replacement and shared rollback/relaunch test seam, using an already validated handle. Renames the running executable to <dest>.old (the one replacement Windows allows on a running image), copies the validated handle over dest, SHA-256-verifies the copy against the authenticated digest, and tries to restore <dest>.old on any failure past the rename — the rename back is retried a few times and then falls back to copying the backup over dest, but if all of that is refused too, dest is left truncated or missing and the reported Op is restore. That is the one outcome PicFetch cannot recover from or even report on the next launch, since reading the record needs the executable that is broken. <dest>.old deliberately survives a successful swap — it is still this process's own running image — for the next launch to sweep (await.go).
applyerr.go ApplyError (Op/Path/Err) and FailureReason (ReasonAccessDenied / ReasonVirusBlocked / ReasonSharingViolation / ReasonUnknown); ClassifyApplyError maps a failed Apply to the reason the next launch reports, preferring Windows errno classification (applyerr_windows.go) over the portable fs.ErrPermission fallback (applyerr_other.go).
await.go AwaitPIDEnv (PICFETCH_UPDATE_AWAIT_PID) relaunch handshake. CleanupPredecessor, called from main.go before app.NewWithID, waits (bounded, 15s) for the process that installed this executable to exit before preferences are touched, then sweeps <dest>.new / <dest>.apply.cmd left by pre-2026-08-30 updates. SweepBackup, called from internal/ui startup once the Fyne app cache exists, removes <dest>.old — skipped when the last recorded apply failure has Op == "restore", the one state where the backup is the user's only intact executable.

internal/preferences

Standing UI preferences via Fyne Preferences (not the session cache). SortMode is a string on disk (filesort.FromPref / Mode.PrefValue). Secondary-window geometry is WindowGeometry structs.

File Responsibility
preferences.go Save, Load, SaveLastUpdateCheckDay, State, WindowGeometry; persists normalized mosaic visual settings and secondary-window geometry, never mosaic sources or display IDs.

internal/appearance

Application-wide System/Light/Dark mode, independent of the viewer. Forced modes wrap Fyne's current theme and override only its color variant; returning to System restores the underlying theme so Fyne's OS appearance watcher keeps switching automatically.

File Responsibility
appearance.go Mode, translated picker labels, stable preference values, and Apply.

internal/distribution

Compile-time distribution policy. StoreManaged is false for ordinary builds and true only with the microsoftstore build tag. internal/ui snapshots it when constructing a viewer so Microsoft Store packages cannot use the GitHub self-updater while all other distributions retain the existing behavior.

internal/wingesture

Pure geometry: timestamped window positions in, spiral Result out. No Fyne, no cgo. Positions are y-down (positive accumulated angle = clockwise on screen). realdrag_test.go replays recorded title-bar drags.

File Responsibility
wingesture.go Direction, Result, Config.
detector.go Ring buffer, idle gap, one-shot armed latch.
analyse.go Centroid, accumulated angle, sign consistency, radius-vs-angle fit.

internal/winpos

Fyne has no position getter and no move event. Get reads the native handle. Tracker remembers the last good reading. Poll / PollAt sample on a background goroutine and queue one native read at a time through fyne.Do. Their Poller separates nonblocking Stop from actual Done / Wait; cancellation discards queued reads without requiring UI to drain. An already-started native read must return before completion. Main-window tracking binds both stop and wait; widgets.Singleton retains unfinished pollers across close/reopen for WaitForTracking.

File Responsibility
winpos.go Get, Set, Maximize, Unmaximize.
poll.go PollAt / Poll / Poller / PollInterval / GestureInterval; per-call native-read/dispatch seam and cancellation-aware acknowledgement.
tracker.go Tracker atomics: Store / Get / Capture / Restore.
darwin.go / windows.go / linux.go / other.go Platform position + maximize. Linux/Wayland: Get reports ok=false.

OS integrations (clipboard, displays, filemanager, filepicker, trash, wallpaper) use dispatcher vars and build-tagged platform files; tests stub them via internal/uitest — see AGENTS.md.

internal/wincom

COM declarations and result handling shared by native Windows adapters. desktopwallpaper_windows.go owns the IDesktopWallpaper GUIDs, vtable layout, and activation constants. Portable monitor.go owns the HRESULT failure predicate and MonitorAttached, which distinguishes an attached monitor (S_OK) from a retained, detached entry (S_FALSE). internal/displays and internal/wallpaper retain their native calls, COM lifetime, feature-specific behavior, and error reporting.

internal/displays

Native attached-display inspection. Inspect(fyne.Window) returns an ordered snapshot of opaque platform IDs, user-facing names, native-pixel bounds, and the display containing the greatest part of the PicFetch window. macOS uses NSScreen/CoreGraphics IDs, Windows uses IDesktopWallpaper monitor device paths while skipping retained detached entries, Linux uses XRandR under X11 and explicitly reports Wayland unsupported. The shared snapshot builder supplies localized fallback names. Callers compare IDs but never parse them.

internal/mosaic

Viewer-independent mosaic generation. Its small public contract snapshots and validates source URIs, native-pixel target size, deterministic seed, and visual settings, then returns immutable rendered pixels. Layout, lazy canonical image loading, coverage, frame/shadow geometry, and rendering stay inside the package. Repeated sources use a generation-local 64 MiB byte cache; oversized sources render without being retained. preparation.go bounds live rendering scratch to 64 MiB separately from output canvases, layout coverage and decoded sources/cache. Ordinary placements retain full Catmull-Rom preparation; oversized working sets prepare visible regions of 256-pixel destination tiles, including resampler/mask storage and bounded extra SVG rasterization. resample.go uses separable Catmull-Rom filtering in 32-row bands, reusing converted source rows and retaining full-precision premultiplied intermediates. The planner accounts for every band/weight/source-row buffer; exceptionally large supports retain the low-storage direct transform when the separable workspace would not fit. Tiles preserve the original affine geometry and use clipped floating-point coverage. GenerateWithProgress reports completed canvas coverage synchronously from the existing layout/render pass; only successful final composition reports completion. preview.go adds independent canvas snapshots after completed placements, capped at a 960-pixel longest edge and four per second. It combines the repair/background and primary layers directly at preview resolution into RGBA storage, selecting the resampler's specialized loops; no full-size snapshot or extra worker is needed. Generation without a callback does no preview work. Rotated masks include a filter margin before clipping so canvas boundaries do not fade. The package has no Grid, display-enumeration, picker, wallpaper, preference, or widget behavior.

internal/clipboard

PNG image data (CopyImage) and file-reference lists (CopyFiles).

File Responsibility
clipboard.go CopyImage dispatcher + per-OS image copy.
copyfiles.go CopyFiles dispatcher + Linux/Windows file-list copy.
darwin.go / other.go AppKit NSPasteboard file list / stub.
windows.go / notwindows.go hideConsoleWindow pair.

internal/filepicker

Native open chooser (Choose) and save panel (ChooseSave). Linux/macOS can pick folders; Windows is files-only.

File Responsibility
filepicker.go Typed Choose (URI list) / ChooseSave (one URI), strict native result decoding, canonical-path Zenity framing and UTF-8 JSON PowerShell transport.
darwin.go / other.go NSOpenPanel / NSSavePanel with a shared NSURL-to-JSON transport / stubs; darwin_test.go exercises the actual native serializer.
windows.go / notwindows.go hideConsoleWindow pair.

internal/trash

Move to Trash/Recycle Bin (Move). Tests use uitest.StubTrashMove.

File Responsibility
trash.go Move dispatcher + Linux/Windows impls.
darwin.go / other.go AppKit recycle / stub.
windows.go / notwindows.go hideConsoleWindow pair.

internal/wallpaper

Set desktop wallpaper through Set(Request), where a zero target retains the legacy global/all-screen action and an opaque display ID requests one screen. UI writes a persistent PNG into its cache before calling this. A typed TargetUnsupportedError distinguishes an honest platform limitation from a native execution failure.

File Responsibility
wallpaper.go Request (including Solo), Set dispatcher, typed limitation, legacy Linux/GNOME/KDE and Windows PowerShell global paths. Linux rejects a target before lookup or mutation unless it is the only attached display. GNOME writes picture-uri and the 42+ picture-uri-dark — the only key on screen in dark mode — skipping the dark key only when the schema genuinely lacks it and otherwise reporting its failure. hostSchemaEnv scrubs GSETTINGS_SCHEMA_DIR / XDG_DATA_HOME / XDG_DATA_DIRS so a snap-wrapped launcher cannot redirect a host desktop write at its own stale bundled schemas.
darwin.go / other.go AppKit all-screen or exact preflighted NSScreen set / non-Darwin stub.
target_windows.go Targeted IDesktopWallpaper: COM-thread lifetime, exact device-path validation, and single-monitor set.
windows.go / notwindows.go hideConsoleWindow pair plus non-Windows target stub.

internal/filemanager

Show one file selected in the current OS's own file manager, through Reveal(path). macOS runs open -R - a LaunchServices binary rather than an Apple Event, so unlike this repo's other macOS integrations it needs no cgo to avoid an Automation prompt. Windows runs explorer.exe /select,"<path>" with the command line built by hand, because os/exec's escaping would quote the /select, prefix along with the path; explorer's own non-zero exit is discarded, which is why the path is stat'ed first. Linux calls org.freedesktop.FileManager1.ShowItems over dbus-send --print-reply (the only portable way to get the file selected) and falls back to xdg-open on its parent directory.

File Responsibility
filemanager.go Reveal dispatcher, the three platform paths, fileURI, and the portable explorerCmdLine.
windows.go / notwindows.go applyExplorerCommandLine pair: SysProcAttr.CmdLine on Windows, no-op elsewhere.

internal/openwith

macOS "Open With", Dock drop, open -a, and double-clicked associations — none of which put files in argv for a bundled .app. AppKit turns the kAEOpenDocuments Apple Event into a delegate call, which Install grafts onto GLFW's delegate class. That event fires inside glfw.Init(), before SetOnStarted, so Deliver buffers until SetHandler installs the viewer's handler and flushes in the same critical section.

File Responsibility
openwith.go The queue (Deliver / SetHandler) and URIsFromFileURLs.
openwith_darwin.{go,h,m} Install / DelegateRespondsToOpen + the application:openURLs: / application:openFiles: graft.
openwith_notdarwin.go Both report false; other OSes use argv.

internal/filescan

Recursive image gather for drop/open, plus a non-recursive sibling listing when the user opened a single file.

File Responsibility
filescan.go Images(ctx, uris, max, progress) (recursive); Siblings(ctx, file, max, progress) (parent dir only, opened file seeded first); symlink-cycle + per-call dedupe.

internal/launch

Command-line flag parsing into the Options value ui.Run applies at startup. Options.ApplicationID owns pre-app Explorer trial offline validation and isolated identity selection; the ordinary app ID passes through unchanged. Hand-rolled rather than flag, so flags may appear anywhere among the paths; rejects an unknown flag, ignores macOS's -psn_*, and validates --sort against the preferences.SortBy* vocabulary. No Fyne import.

File Responsibility
launch.go Options, Options.ApplicationID, Parse, Usage, ErrHelp; the flagSpecs table every flag is declared in.

internal/filesort

Five orderings the S key cycles, plus Label (lang.L) and preference string translation (FromPref / PrefValue).

File Responsibility
filesort.go Mode, Next, Order, Label, FromPref / PrefValue.

internal/fileidentity

occurrence.go owns immutable path-plus-ordinal bookmarks and a captured index for exact occurrence lookup. Grid and image visits share this contract. Missing occurrences return absence; each caller chooses its fallback. The package has no Fyne or filesystem I/O and does not identify source-content versions.

internal/selection

Integer index set + range anchor for grid multi-select. No Fyne import.

File Responsibility
selection.go Set, Toggle, Add, Range.

internal/dupes

Which files in a file set duplicate which others. Fyne-free — reached through a FileSet interface with string keys, not fyne.URISnapshot() Snapshot is FileSet's only method, handing back an immutable {keys, generation, key→index} view rather than answering a live count and key lookup — and its only project import is internal/imaging. Owns dHashes and native pixel sizes keyed by file (generation-scoped: WipeIfStale on a fresh drop, AdoptGeneration on an incremental shrink), the Hamming distance threshold, the installed group snapshot (representative = highest native pixel count, lowest index on a tie), the hide-duplicates and inspect modes, and the visibility queries (IsVisible / NextVisible / FirstVisible / LastVisible / VisibleIndexesExcept) plain navigation asks. A caller testing many indices reads the model once via Visibility(), a frozen hide-flag-plus-groups value with its own HiddenExtra / Visible / RepresentativeOf / Size. internal/ui owns the Model and implements FileSet; internal/ui/grid reads and feeds it (hashing pass, browse, badges) but does not own it.

File Responsibility
dupes.go Model, FileSet, hash/native facts, generation (WipeIfStale / AdoptGeneration, read through a Snapshot), distance clamp, OnChange observers.
facts.go FactWriter captured before source work; generation and reset admission share each fact mutation's lock. Reads ignore a mismatched namespace so explicit adoption can retain established facts.
groups.go Groups snapshot, GroupingKey, ComputeContext / guarded Install / reusable Rebuild, GroupSize / RepresentativeOf / Members.
snapshot.go Snapshot: the immutable {keys, generation, key→index} view every Model method reads through.
visible.go Hide/inspect modes (SetHideDuplicates, BeginInspect / ClearInspect / InspectMembers), IsHiddenExtra, Visibility, and the visibility/navigation queries.

internal/decodepool

Bounded demand-started workers + per-key in-flight claim. A lifecycle-bound Queue removes its pending jobs on cancellation and invokes their callbacks with failed admission; it keeps current interactive jobs ahead of low-priority background work without creating a goroutine per queued source. Grid thumbs: Pool[*fyne.Container, thumbClaim]. Display preloads: Pool[string, struct{}]. Cell staleness stays in grid/thumbs.go.

File Responsibility
decodepool.go Pool[K,V], Claim / Release, Begin / Queue.Go / Queue.GoLow, one-job Go, Wait.

internal/completion

One-shot “this background op finished” signal. Named wait helpers vs drain: see AGENTS.md § Concurrency and Fyne.

File Responsibility
completion.go Signal (Begin / Wait / Begun / Current) and Handle.

internal/uitest

Test-only fixtures and OS-seam stubs. Never imported from production files.

File Responsibility
uitest.go Temp URIs, synthetic images (including animated GIF, EXIF-oriented/GPS/dimension-tagged JPEG, RAW preview, SVG), ExifIFD0HasTag for asserting on a written file's tags, ApproxEqual.
stubs.go StubChooser, StubSaveChooser, StubClipboardCopy / CopyFiles, StubTrashMove, StubWallpaperSet.
uiqueue.go Drainable UIQueue.
reader.go Instance-owned ReaderURI sources, ReadCloser callbacks and DirectoryURI listings for holding reads across cancellation; one immutable repository adapter registered before workers start.

Wait helpers (waitUntilLoaded, dropAndWait, …) stay in internal/ui/harness_test.go.

Translations

main.go owns //go:embed translations/*.json and lang.AddTranslationsFS. filesort.Label and internal/filepicker are the lang.L call sites outside the UI tree. main_test.go checks locale parity and that en.json is an identity map. String rule: see AGENTS.md.

Error handling

fyne.LogError is used in main.go, internal/ui glue, and internal/session. Viewer-independent packages return errors. Ignore rule: see AGENTS.md.

Where to look for X

  • "How is an image loaded/decoded/cached?" → internal/imaging/loader.go (raw.go for camera-RAW previews).
  • "How is image memory bounded, and where are the limits set?" → internal/imaging/bytecache.go + internal/ui/memlimits.go + gif.go + loader.go MaxEncodedBytes.
  • "Where does the EXIF panel live?" → internal/ui/exifwin.
  • "Who owns the Hypno Spiral?" → internal/ui/tunnel.go freezes sources for internal/ui/spiral; Help emits the secret phrase callback and the gesture uses the same viewer-owned window.
  • "Why is the log not full of tile fetch error?" → internal/ui/exifwin/tiles.go quietPendingTiles / tileLogFilter.
  • "Why doesn't the EXIF window's map freeze the app while it loads?" → exifwin/tiles.go + startWarm / syncLoading.
  • "Why is the info overlay's 'Show EXIF data' link missing?" → info.go syncInfoOverlayVisibility + internal/ui/infoview Card.Sync / HasEXIF.
  • "How does the persistent info overlay (I key) work?" → internal/ui/infoview + info.go.
  • "Why is a menu item greyed out (Checked/Disabled)?" → internal/ui/menus Apply + menu.go menuState / syncMenus.
  • "Where is a photo's GPS position read, and where is it shown?" → internal/imaging/exif.go parseGPSIFD + exifwin Location section.
  • "How is EXIF orientation handled?" → internal/imaging/exif.go + orientation.go.
  • "How is a camera RAW file shown?" → internal/imaging/raw.go + LoadedImage.Preview + load.go / info.go.
  • "How does drag-and-drop / folder scanning work?" → internal/filescan.Images / filescan.Siblings (single-file case) + drop.go handleDrop.
  • "How is an image shown/preloaded/animated?" → internal/ui/display; root load.go owns navigation/window policy.
  • "Which keys do what?" → keys.go (handleKeyEvent / handleTypedRune) + shortcuts.go.
  • "How do I find one file by name in a big drop?" → internal/ui/grid/search.go + keys.go handleTypedRune.
  • "How does hide-duplicates work?" → internal/dupes (the model: grouping, hide/inspect modes, visibility — BeginInspect / InspectMembers / IsHiddenExtra / NextVisible / Visibility) + internal/imaging/dhash.go (the hash) + internal/imaging/grouping.go (cancellable indexed complete linkage) + internal/ui/grid/hashengine.go (the pool-driven pass that fills the model) + grid/dupes.go (browse). Escape reopen: internal/ui reopenVariantGrid.
  • "How do I act on several images at once?" → internal/selection + grid/selection.go Targets + grid/marquee.go + batch.go + deletion.RequestFiles / clipboard.CopyFiles.
  • "How does zoom/pan work?" → internal/ui/zoom; keys in keys.go; window resize in load.go syncWindowToZoom.
  • "How does an SVG stay sharp when I zoom?" → internal/imaging/vector.go RasterAt + svg.go + internal/ui/display/vector.go + root density forwarding and zoom SetLogicalSize / onScaleChanged.
  • "How does rotation work, and how is it saved to disk?" → internal/ui/display (frames/rotation state) + internal/ui/rotate.go + internal/ui/save.go + internal/imaging/save.go.
  • "How do I write an image out in a different format, at a smaller size, or without camera metadata?" → internal/ui/export.go + internal/ui/exportoptions.go + filepicker.ChooseSave + imaging.Export/ExportOptions.
  • "How do I open the current image's folder in Finder/Explorer/my file manager?" -> internal/ui/reveal.go + internal/filemanager + shortcuts.go wireRevealShortcut + internal/ui/infoview RevealLink.
  • "How does 'Set as Wallpaper' work?" → internal/ui/wallpaper.go + internal/wallpaper.
  • "How is an image mosaic sourced, generated, previewed, exported, and targeted to a display?" → internal/ui/mosaic.go + internal/ui/mosaicwin + internal/mosaic + internal/displays + internal/ui/wallpaper.go.
  • "How does the slideshow / picture-frame mode work?" → internal/ui/slideshow + slideshow.go.
  • "How does delete work?" → internal/ui/deletion + internal/trash + shortcuts.go / batch.go requestDelete.
  • "How are native file dialogs implemented?" → internal/filepicker + openfiles.go / export.go.
  • "How is the last session saved/restored?" → internal/session + session.go restoreSession.
  • "How do in-app updates work?" → internal/update + internal/ui/autoupdate (serialized automatic/manual checks, staging, apply intent, What's-New cache, apply-failure cache) + internal/ui/autoupdate.go (maybeStartUpdateCheck / CheckForUpdatesNow / PerformUpdate / maybeShowWhatsNew / maybeShowUpdateFailure) + settingswin (manual dialogs) + help/whatsnew.go (the window). Automatic checks are off by default (preferences.CheckForUpdates) and stage silently. Apply remains OnStopped: normal shutdown installs without relaunch; explicit Perform update adds a post-apply relaunch. On Windows that relaunch starts the new executable with PICFETCH_UPDATE_AWAIT_PID set to the installing process's PID; update.CleanupPredecessor (internal/update/await.go), called from main.go before app.NewWithID, waits on that PID before preferences are touched, unsets the variable, and sweeps leftovers from pre-2026-08-30 updates. If update.Apply fails, ClassifyApplyError (internal/update/applyerr.go) records the reason via autoupdate.SaveApplyFailure, and maybeShowUpdateFailure explains it on the next launch with a button to the releases page — releases are unsigned, so Controlled Folder Access can still deny the write even to picfetch.exe itself. GitHub TUF bootstrap expiry: tufroot.go.
  • "How are GitHub release notes written?" → todos.md ## Done + scripts/releasenotes + make release + .github/workflows/release.yml body_path.
  • "How are Linux race-test shards measured and assigned?" → scripts/testshards + .github/testshards/internal-ui.tsv + the measured CI sharding plan.
  • "How is a WinGet publish gated after Release?" → .github/workflows/winget.yml + scripts/wingettag (vX.Y.Z allowlist; workflow_run must be release.yml on a published tag).
  • "How are Microsoft Store updates submitted and reconciled?" → scripts/storepublish + .github/workflows/microsoft-store-publish.yml + docs/microsoft-store.md.
  • "How does a macOS Open With reach the viewer?" → internal/openwith (queue + Objective-C graft) + main.go openwith.Install + internal/ui/openwith.go + run.go SetOnStarted.
  • "How does the packaged macOS app declare file/folder associations (Open With)?" → internal/imaging/loader.go SupportedExtensions + scripts/plistdoctypes + Makefile package-mac.
  • "How do Favorites work?" → internal/favstore + internal/ui/favorites + shortcuts.go + viewer.OpenFavorite.
  • "How are favorite previews cached on disk?" → internal/favthumbs + internal/ui/favthumbs.go + favorites + grid thumb accessors.
  • "Where is the File menu / Settings window?" → menu.go buildMainMenu + actionmenu.go + settingswin + viewer.settingsState / ApplySettings + viewer.closeFiles.
  • "How are preferences (sort order, appearance, merge mode, slideshow interval/shuffle, folder-scan cap, window-size cap, static window size, window size/position, favorite-preview-cache toggle, similarity cache/automatic update/fit settings, check-for-updates checkbox) persisted?" → internal/preferences + startup.go + features.go + windowtrack.go + run.go currentPreferences.
  • "How does Light/Dark/System appearance work?" → internal/appearance + internal/ui/theme.go + settingswin.
  • "How do the Settings and EXIF windows come back where I left them?" → widgets.Singleton.Remember / Geometry / StopTracking + winpos.Poll + preferences.WindowGeometry.
  • "How is the window's on-screen position read back, since Fyne has no getter for it?" → internal/winpos + windowtrack.go startWindowPosPolling.
  • "How can dragging the window open something?" → internal/wingesture + gesture.go + openSpiralForGesture / spiral.ShowForGesture.
  • "How does copy-image-to-clipboard work?" → internal/clipboard + clipboard.go. Batch file copy: copyfiles.go + batch.go copySelection.
  • "How does Copy Selection (image-region copy) work?" → internal/ui/copyselection (Source / Encode, HandleKey) + copyselection.go (pause, clipboard worker, yieldCopySelection) + menu.go yieldingMenuCallbacks + shortcuts.go yieldingShortcuts + zoom Geometry / HandleScroll + overlay order in build.go.
  • "How does two-image comparison open, preserve raster/vector fidelity, render through stable overview/detail shaders, identify/swap sides, switch side-by-side/swipe, toggle photo editing with physical Ctrl+L, route camera/photo/divider input, isolate commands, load, settle, and return to the grid?" -> internal/ui/compare (renderer.go scene/source seam, shader.go stable GPU panes and tile lifecycle, tile.go reveal-aware planning/cache/generation, plus surface/chrome/reveal clips/layout/two photo transforms/shared camera/hover target/divider/input shield/load/vector lifecycles/UI queue) + internal/ui/compare.go (selection, focus release, canonical loader, title, active-mode fact, and open refusal) + features.go / keys.go / shortcuts.go (physical Ctrl+D, toggle hook, transform-key, divider-key, and command routing) + favorites / menus / menu.go (disabled state and guarded entries) + open paths in drop.go / openfiles.go / openwith.go + overlay order in build.go.
  • "How does the grid overview / thumbnail generation work?" → imaging/thumbnail.go + grid/grid.go + grid/thumbs.go + grid/hashengine.go + grid/nav.go + grid/uiqueue.go.
  • "What decides the window title?" → viewer.go setTitle / applyTitle / HighlightChanged + load.go + grid/nav.go.
  • "How do I write a test that needs an image / a viewer?" → internal/uitest + newTestViewer / newTestUI + dropAndWait in harness_test.go.
  • "How do I add or translate a user-visible string?" → lang.L at the call site and the same key in every translations/ bundle. See AGENTS.md.
  • "Why isn't feature X its own package?" → AGENTS.md.
  • "How/where are errors reported, and when is it OK to ignore one?" → AGENTS.md.

Keeping this doc current

Update this file in the same change when a package is added, removed, renamed, or files move between packages. Cells are locators (path, symbol, order) — one sentence. Standing rules go in AGENTS.md, not here.