Releases: frathe/picfetch
Release list
v0.2.12
What's Changed
New Features
- Added manual update checks in Settings: Check now bypasses the automatic
daily gate, shows checking/current/download/error states, verifies and
stages updates, and offers Later or Perform update to apply and
relaunch with What's New on the updated launch.
Internal
-
Qodana's false-positive suppressions are confirmed in CI, not just under
GoLand's engine: at210fee5(run33270269940), the summary CSV counts
the 12 findings covered by the 8//goland:noinspectioncomments exactly —
GoMaybeNil2,GoBoolExpressions3,GoRedundantConversion1,
GoErrorStringFormat2,GoVarAndConstTypeMayBeOmitted4, summing to 12 —
and none of the 12 appear anywhere inqodana.sarif.json's results, whose
onlyruleIdvalues areDuplicatedCodeandGoTypeAssertionOnErrors:
12-of-12 suppressed. The other 18 suppressions, theGoUnusedParameter_
renames, never produced a CSV row at all — the blank identifier removes the
finding before the inspection pass counts it, rather than suppressing a
counted one. The earlier prediction that the next CI run would show "30
fewer problems (155 -> 125 on a full scan)" was wrong, and not because the
suppressions failed: 155 was an IDE Project Default scan and 125 assumed CI
would run that same profile, but CI runsqodana.starterinstead, so an
IDE Project Default total and a CIqodana.startertotal were never
comparable in the first place. -
CI does not under-report duplication against a full scan — the old framing
had it backwards. Qodana emits one SARIF result per duplicate cluster,
not per fragment: at210fee5the SARIF holds 33DuplicatedCoderesults
occupying 71 location slots, and the slots are not disjoint because 8
fragments each belong to 2 clusters (55 fragments in exactly 1 cluster, 8
in exactly 2: 55×1 + 8×2 = 71), so 71 slots reduce to 63 distinct
fragments. The summary CSV counts 75DuplicatedCoderows, and the gap
closes completely rather than leaving a mystery: 75 CSV rows = 71
test-file fragments + 4 production fragments already suppressed at source
in the orientation pixel loops (see Orientation transforms below), and 71
− 8 serialisation losses = 63 SARIF fragments, so the 12-fragment
CSV-to-SARIF gap is 8 + 4 with nothing left unexplained. At210fee5the
IDE finds 71DuplicatedCodefragments and CI finds 63; CI's 63 are a
strict subset of the IDE's 71, and the 8 fragments CI is missing — 7 in
internal/imaging/loader_test.goand 1 ininternal/update/tufroot_test.go
— are accounted for by a genuine Qodana serialisation failure that the CI
run recorded itself as 3DuplicatesProblem"Can't find duplicate problem
in db" warnings naming exactly those 2 files and no others. All 33
clusters at210fee5were test-only — every fragment in every cluster
lives in a_test.gofile — soqodana.yamlnow excludes those files from
DuplicatedCodeby explicit path; run33274422606ated3d4e6confirmed
it, returning 0 SARIF results. -
Making the
DuplicatedCodeexclusion actually take effect needed three
attempts, but the mechanism itself was never broken — only the pattern.
exclude:with apaths:glob compiles into a real scope:
log/effective.profile.xmlshows<scope name="qodana.yaml.exclude.DuplicatedCode" level="INFORMATION" enabled="false" />nested inside theDuplicatedCodeinspection element
itself, proving the entry reached the engine — but a compiled, disabled
scope that matches no file suppresses nothing. Both"**/*_test.go"(run
33273666731atd38f6e8) and"**_test.go"(run33274030031at
f481c4a) failed exactly that way: 33DuplicatedCoderesults both times,
unchanged from baseline. The second glob is the dialect JetBrains uses for
its own built-in scopes (glob:**.md,glob:**.test.ts), so it was not a
wild guess, and it still did not match. Neither failure was a delivery
problem — each run'slog/qodana-config.jsonechoes back the exact
pattern that run was given, and neither run'sidea.logshows a cache hit
or restore. Explicit file paths work: withqodana.yaml'spaths:
replaced by the 30 flagged test files listed by path, run33274422606at
ed3d4e6returned 0 SARIF results. That the CSV'sDuplicatedCodecount
fell to 4 rather than 0 on that run is the proof the exclusion narrowed the
inspection instead of disabling it outright — a full disable would have
zeroed the CSV too, not just the SARIF. -
Update-check lifecycle ordering is restored: after its existing gates,
maybeStartUpdateCheckprepares the verifier/client through the
instance-ownedUpdaterverifier-factory seam beforeupdateOp.begin(),
then callsUpdater.Start. The focused
TestUpdateCheck_VerifierFailurePreservesLifecycleregression test proves
a verifier failure preserves the revision and prior token/context, leaves
the client nil, and never begins completion. Movingbeginabove
EnsureClientmakes that test fail on the lifecycle preservation; restoring
the intended order passes. -
buildMainMenu's exacthelp.SetOnManualOpened(view.syncMenus)observer
registration is covered by
TestBuildMainMenu_ManualOpenedObserverSyncsWindowHelp. The test scopes
theme.DefaultTheme()to this case because the shared Fyne test theme
cannot render the manual's complete Markdown font combinations; it identifies
and closes the newly created manual window and restores the original theme
during cleanup. Removing the observer registration makes the test fail,
leaving Window → Help enabled after the manual opens; the restored
registration passes. -
Favorites no longer un-merges the macOS native menu bar. The original note
here had the diagnosis half wrong, so for the record:SetHasFileswas
never an unguarded site -syncMenusfolds on the very next line,
deliberately -SetDiris covered by the startup fold atrun.go:45, and
there is no rename path at all, sincefavstoreexposes onlySaveand
Remove. The two real paths were adding a favorite (favorites.go:249)
and deleting one (manage.go:362), both reachingrefreshMenuwith
nothing to fold the bar afterwards.favorites.Hostgained
RefreshMenus, which the viewer implements asrefreshMainMenu;
refreshMenucalls it instead off.menu.Refresh, andSetHasFilesnow
only flipsDisabled. The invariant is structural rather than documented:
internal/ui/favoritesnever callsfyne.Menu.Refresh, so
refreshMainMenuis the only place a main-bar menu is re-published.
Pinned at both ends - three tests infavoritesthat the feature makes
the call (and thatSetHasFilesdoes not), one ininternal/uithat the
viewer's method reachesrefreshMainMenu. Fyne's test driver records
nothing when the bar is re-published:MainMenu.Refreshre-hands the same
pointer totest.window.SetMainMenu, which just stores it, and both
syncNativeMenuBarsteps dead-end on NSApp's nil menus in a test binary.
The only trace isrefreshMainMenureading the window's menu, so the
viewer test observes that through a decorator on the per-viewerv.win.
All four verified by mutation. -
Small mechanical Qodana fixes, five categories.
imaging/save.goand
exifwin/tiles_test.gocompare errors witherrors.Isnow;
update/extract.goandplistdoctypes/doctypes_test.godeliberately keep
err == io.EOF, because the inspection does not flag the documented
tar.Reader.Next/xml.Decoder.Tokenloop terminators and the bare
sentinel is what those APIs promise.raw_test.godrops a redundant
[]byteconversion (copytakes a string source).apply_test.go's
localrealbecametarget, filename string included, so it no longer
shadows the builtin. The two scripts mark their ignoredFprintfresults
_, _ =, matchingscripts/plistdoctypes/main.go. The spiral FPS
backdrop colours were the only non-trivial one: they were positional
color.NRGBAliterals written out in bothoverlays.goand
overlays_test.go, so they moved into package-level
fpsGoodColor/fpsWarnColor/fpsBadColorthat both sides name. That
alone would have gutted the test - it would compare a var against itself
and only catch a wrong threshold - soTestFPSBackdropColorValueswas
added to pin the values, and verified by mutation: bumping
fpsGoodColor's G to 121 fails it while the threshold test still passes. -
Doc-comment openings:
GoCommentStartwants the documented element's name
as the comment's first token, followed by whitespace - an optional leading
A/An/Theis the only thing allowed in front. Punctuation glued to
the name breaks it - a slash, a comma, or a colon all fail even when the
comment already opens with the right word, which is whyDir/SetDirand
MouseIn, MouseMoved, and MouseOutwere both flagged. The slash-joined
pairs became theX and Yformzoom.In/Out,
widgets.FocusGained/FocusLostandimaging.MinVectorWidth/Height
already used; the threewidgets/style.gogroup comments now lead with
the first constant in theirconst/vargroup; andinternal/ui's five
file-level notes gained a blank line beforepackage ui, which detaches
them from the package clause - the form 69 other files in the tree already
use, withrun.gokeeping the one real package doc.
spiral.ShowForGestureandShowshared one comment block that opened by
describingShow, so it was split:go doc Spiral.Showprinted nothing
before and prints its paragraph now. Also normalized the six unflagged
sites with the same shape inload.goandpreferences.go; the
inspection skips struct fields and mid-comment references, so those were
consistency rather than findings. Verified through GoLand's inspection
engine over all 13 files, which is the same engine Qodana...
v0.2.11
What's Changed
New Features
- Changed a few labels to make them less confusing
Bugfix
- Hashing a folder no longer crashes when the file set shrinks underneath
the pool. The duplicate model reads an immutable snapshot of the file
set instead of the live slice — the old liveCount()/KeyAt(i)pair
could index past the end, reproducible in about a second on a 48-file
drop under-race. - A file the scanner produced no URI for no longer panics the hashing
pass. - A crash when pressing 0 after rotating an image and returning to the
drop zone.clearToDropzonecleared the frames but left the rotation
set, and the 0 key is handled ahead of the navigation guard, so
resetRotationindexed an empty frame slice. Reproducible in four
keystrokes: open, R, Escape, 0.
Internal
internal/dupes.FileSetis a singleSnapshot()method; everyModel
method takes one snapshot at entry.- Arrow keys with inspect or hide-duplicates on no longer rescan the whole
file set per step (InspectSourceis an O(1) map lookup; the
hidden-extra walk takes the model mutex once per call, not once per
candidate). - A new hashing pass no longer inherits the previous pass's hide-apply
throttle floor. - Deleted
grid/dupes.go'srememberHashFailandhashFailedOf, two
duplicate-hash wrappers with no callers anywhere in the tree; the
hashing pass already callsdupes.Model.PutFailed/Faileddirectly.
Dropped the stalehashFailedOfmention fromhashengine.go's
wrapper comment. internal/dupesnow exportsVisibility(), a frozen read of the
hide flag plus the installed group snapshot taken in one model-mutex
acquisition, withHiddenExtra/Visible/RepresentativeOf/Size
methods for testing many indices against it without re-locking per
index.applyVisibleFilter's filter pass andjumpIfHiddenExtranow
take one such read instead of one (in fact two) model-mutex
acquisitions per file.- Split the
viewergod object from 87 fields to 55: four new
subpackages, each reading a valueStatesnapshot built in exactly one
internal/uifunction rather than aHostinterface —
internal/ui/menus(the 20 File/Window/Actions menu items and their
whole Checked/Disabled matrix asApply(State), −19 fields),
internal/ui/autoupdate(the release check/download policy, staged-
update lifecycle, and What's-New cache, −4 fields),internal/ui/infoview
(the persistent info overlay's widgets and text formatting, −6 fields),
andinternal/ui/display(decoded frames, frame index, view-only
rotation, and crossfade, −3 fields). AHostformenusalone would
have needed roughly a dozen methods, which is why these four read a
snapshot instead. Also lands needs_refactoring.md item 5 (menuState()/
syncMenus()replace the per-site Checked/Disabled push and
HighlightChanged's four-boolean hand-diff with one recompute-and-diff
entry point, called from 16 choke points instead of the previous 23 call
sites) and the rest of item 6 (appStatenow evicts the image cache
itself on file removal via anonRemovehook, wired inbuild.go).
Full Changelog: v0.2.10...v0.2.11
v0.2.10
What's Changed
Bugfix
- macOS "Open With", dropping a file on the Dock icon,
open -a, and
double-clicking a file already associated with PicFetch now actually open
it in the app — every one of those was silently ignored before, whether
PicFetch was already running or being launched cold by the click itself.
These paths also now cover the app's whole supported format list, not just
the seven extensions Finder used to offer them for: HEIC, AVIF, TIFF, SVG,
ICO, BMP, and every RAW format PicFetch reads are all included now, and a
folder can be dropped on the Dock icon the same as a file.
Internal
- Extracted the duplicate-visibility model out of
internal/ui/gridinto a
new, viewer-independentinternal/dupespackage: dHashes and native pixel
sizes keyed by file, generation-scoped wipe-vs-adopt, the Hamming
threshold, the group snapshot (Compute/Install), the hide-duplicates
and inspect modes, and the visibility queries (IsVisible/NextVisible/
FirstVisible/LastVisible/VisibleIndexesExcept) plain navigation
needs. The viewer now owns the model and answers arrow-key/Home/End/
shuffle questions directly from it (internal/ui/visibility.go) instead
of polling a closed grid overlay.internal/ui/gridkeeps presentation
and browse-duplicates, and boxes its hashing pass behind a new
hashEnginetype (grid/hashengine.go) rather than more fields on
Overview.
Full Changelog: v0.2.9...v0.2.10
v0.2.9
What's Changed
Bugfix
- Reject zip/tar entries that are not
filepath.IsLocalduring update extract (GitHub CodeQL go/zipslip).
Internal
- Drop deprecated
tar.TypeRegAin update extract (still accept the historic NUL regular-file typeflag). - Install CI's Linux GUI packages in CodeQL so
internal/winpos/linux.gocan compile.
Full Changelog: v0.2.8...v0.2.9
v0.2.8
What's Changed
New Features
-
When only a single image is selected or dropped,
the left and right arrow keys now move to the next/previous image in the same folder. -
Loop duplicate variants after picking one from the grid.
Return from the variants grid kept losing the chosen extra to the
highest-resolution stand-in, and arrows then skipped the rest of the
group. Committing a variant now inspects that file, wraps arrows inside
the group, and uses Escape to walk back to the variants grid.
Bugfix
- When duplicates are hidden, the highest resolution image will now be selected by default.
Internal
make releasewrites GitHub release notes from this Done section (empty categories dropped) and appends the Full Changelog compare link.
Full Changelog: v0.2.7...v0.2.8
v0.2.7
What's Changed
New Features
- Rectangle selection in the grid overview: click-and-drag draws a selection rectangle and picks every thumbnail the rectangle touches. A click without dragging still opens the image. Hold
ShiftorCmd/Ctrlwhile dragging to add to the current selection instead of replacing it.Escduring a drag cancels it; the grid stays open after you release.
Full Changelog: v0.2.6...v0.2.7
v0.2.6
What's Changed
Features
- Automatic updates: Opt-In via App-Settings, checks once a day
Full Changelog: v0.2.5...v0.2.6
v0.2.5
What's Changed
Bugfix
- Fix fyne install by declaring the app icon in FyneApp.toml
Full Changelog: v0.2.4...v0.2.5
v0.2.4
What's Changed
New Features
- New Window and Actions menus: Functionality previously available only through keyboard shortcuts is now also accessible from the new Window and Actions menus.
Bugfix
- The Remove Metadata button is now hidden when no metadata exists.
Internal
- Several small refactorings.
Full Changelog: v0.2.3...v0.2.4
v0.2.3
What's Changed
New Features
-
Hide extra copies of the same shot (D). Uniques stay visible; each group keeps one representative (the earliest file in the current order). Remaining cells show a count badge. The full set stays loaded — this is not a duplicates-only gallery. Closing the grid leaves hide on, so arrows,
Home/End, and picture-frame advance keep skipping extras until you pressDagain -
Browse every copy of the current shot ((Shift + D). The grid lists that group only, including extras
Dwould hide. A unique shot does nothing. While thumbnails are still being hashed, a toast says the images are being analyzed and the group appears when that finishes -
Duplicate match distance in File → Settings… (0–32, default 6; lower is stricter, 0 is an exact thumbnail hash). Changing the slider while extras are hidden regroups immediately. Re-saved and downscaled copies of one picture match at the default; cropped versions are not detected
-
Two files count as the same shot only when every pair in the group is close enough. A chain of similar-looking photos does not merge into one giant group, and solid-color images are left ungrouped
-
The count badge is a bold number on a black chip in the top-right of the cell, inset so the highlight ring does not run through it
/search stacks with hide-duplicates. While the search bar is open,Dis a letter, not a toggle.Escclears one layer at a time: selection, then search, then browse, then hide, then the grid.
Bugfixes
- Pressing
Don a large folder no longer freezes the window until hashing finishes. Hiding duplicates shows immediately, extras drop out as hashes land, and the highlight stays on the same file instead of jumping to the top
Internal
- Grid thumbnail and hash completions marshal through a per-instance UI queue so tests can drain them instead of racing Fyne's test driver
Full Changelog: v0.2.2...v0.2.3


