Skip to content

nfty 1.5.1 — one mark, and a reorder that waits for its own write

Choose a tag to compare

@corderro-artz corderro-artz released this 15 Sep 23:34
· 14 commits to main since this release

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 through MoveNodeToAsync carrying no guard at all — the same door
    MoveLayerAsync was 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.PendingReorder is the task the gesture started, which is the one
    observable trace an async void handler 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.