nfty 1.5.1 — one mark, and a reorder that waits for its own write
One mark instead of two, and a reorder that waits for its own write.
2,220 tests, 0 failures.
The brand mark is the application icon's own symbol
The product wore two marks and nothing in the code said they were meant to agree: a
lowercase "n" turned 45° in the titlebar, and a plain rotated square on the quick-reference
sheet — which is what the titlebar's own comment says it had replaced. The sheet's had
already drifted.
A letter rotated into a diamond spells the name and says nothing about what the app makes.
The mark is a stack of layers with the top one live, which is what an asset is here —
drawn once in Views/BrandMarkView.axaml and used by both. Its ink is AccentBrush over
FgBrush, so light and dark are one drawing rather than two, exactly the way the icon set
already works.
It carries two layers where the 256px card carries three, and the shipped 64px favicon
makes the same cut: a third row at 24px is a smear rather than a layer.
tools/icons/make-app-icon.py redraws nfty.ico from the theme's own token values rather
than exporting a copy, and keeps its per-size tuning — the small sizes now vary how many
layers rather than a letter's contrast. The .ico shows the symbol on the app's washed
tile rather than the card, because the wordmark is unreadable below 64 and a near-black
ground vanishes into a dark taskbar, which is the size and the place this is seen most.
TextBlock.brandmark is deleted with the glyph it described. The README's second badge
points at the brand art in the site repo, closing the backlog entry that asked for it.
A locked file was the disguise on a real race
ExplorerTreeReorderTests' drop test failed about four runs in six with an IOException
out of Cleanup — the temp directory deleted while the persist still held
book.cbk.<guid>.tmp open. That reads as a file-locking nuisance and is nothing of the kind.
A teardown that throws replaces the assertion failure underneath it. The drop handler is
async void, so MouseUp returns the instant PersistAsync reaches its first await — the
assertion on the moved stack was failing too, and the finally's exception took its place.
Dispatcher.UIThread.RunJobs() drains what has been posted and returns; it is not a wait,
which is why this tracked disk speed and passed nine times running on a fast machine.
Two fixes, each probed by breaking it:
- Every reorder shares one in-flight flag. The tree's drag and its reentrant
Alt+Up
chord arrived throughMoveNodeToAsynccarrying no guard at all — the same door
MoveLayerAsyncwas already guarded for, reopened one screen over. They all write the
whole book back to one archive, so two in flight collide whichever gesture started them.
One flag, not one per gesture: a second would only make each door safe against itself.
Refused rather than queued — a queued move is computed against a stack the reader can no
longer see. ExplorerView.PendingReorderis the task the gesture started, which is the one
observable trace anasync voidhandler leaves. The gesture tests await it instead of
pumping once or polling on a timer; a sleep-until-it-looks-right loop passes for the wrong
reason on a fast disk.
The guard is pinned by re-entering from inside ICookBookSession.Replace — the one moment a
persist is provably mid-flight — because starting a second gesture from the test body and
hoping it overlaps proves nothing on the machine where it does not.
Downloads
| Size | .NET 10 needed | |
|---|---|---|
| Portable | 84 MB | no — unzip anywhere and run. Start here if you are unsure. |
| Single file | 76 MB | no — one .exe. Unpacks itself on first run, so that launch is slower. |
| Single file, .NET | 14 MB | yes — one .exe, runtime left out. |
| Framework-dependent | 14 MB | yes — the same, as a folder. |
The two .NET builds need the .NET 10 desktop
runtime; the other two carry everything.