Releases: vdavid/prvw
Releases · vdavid/prvw
Prvw v0.13.0
Added
- View → Sort by → {Name | Date | File type}. Name (natural alphanumeric, case-insensitive) is the default and fixes
thephoto_1, photo_10, photo_11, photo_2problem —photo_2now sorts beforephoto_10. All comparators ascending;
Name is the tiebreaker for Date (file mtime) and File type (lowercased extension, empty extension first). Persists
across launches. Changing the sort updates the in-memory cache transparently: in-window decoded images stay,
out-of-window entries get evicted, missing in-window slots get queued for preload — loop navigation respected. The
underlying path-keyed cache (refactor that makes this work) also unblocks a future directory-watcher rescan
(9a80ec3e,
bb95700e,
04ca1311).
Changed
- Release script runs full
./scripts/check.shbefore bumping the version, so lint/format/test regressions and the
changelog-linksvalidator (fabricated commit SHAs) abort the release before any tag exists. Also auto-detaches stale
/Volumes/Prvw*mounts and brings the self-hosted runner back up viasvc.sh startif its LaunchAgent died — both
scenarios broke v0.12.0's first attempt (eb9b4196). - Release skill caffeinates the MacBook through the build (
caffeinate -i, idle-sleep only) and verifies the GitHub
Release assets +https://getprvw.com/latest.jsonafter the run succeeds, surfacing webhook deploy failures without
blocking release success (d09d6471).
Fixed
- Flaky integration tests under load.
histogram_hover_bin_updatesandnarrow_window_overlays_dont_crashused
fixedsleep(100..200ms)between an MCP/HTTP command and the state-read assertion; under release-script load (full
check suite + nextest parallelism on a busy machine), processing exceeded the sleep budget and the assertion fired on
stale state. Both now poll via the existingwait_for_statehelper. Surfaced during v0.13.0 release prep
(5a567c0e).
Prvw v0.12.0
Added
- RGB histogram overlay. Toggle via View → Histogram or bare
H. Top-right of the window, rounded translucent
backdrop matching the existing pill style, off by default. Hover the histogram for per-bin R/G/B counts. Computed
lazily on first toggle and cached per-image; rayon-parallel scan handles 20+ MP buffers in a few ms. Persists
across launches (d811bb32,
a6c833c6). - EXIF info overlay. Toggle via View → Exif info or bare
E. Below the histogram with a small gap when both
visible, same width and backdrop. Curated photographer-friendly grouping: camera, exposure triplet
(1/250 s f/2.8 ISO 400), lens, date taken, image dimensions, software, GPS. Hidden entirely on formats with no
EXIF data. Long values wrap and grow the panel while preserving the row pitch. Backed bynom-exiffor JPEG /
Generic andrawler::RawMetadata::exiffor RAW (419c6d54,
a6c833c6,
6846416a). - Loop navigation. Toggle via Navigate → Loop navigation or bare
L. At the last image Next wraps to the first
and vice versa. The preloader is wrap-aware too — toggling on near a directory edge triggers preloads of the
wrap-side indices, toggling off evicts them, so the cache always matches the active window. Off by default,
persists (88b37740). - Home / End jump to first / last image. Navigate menu gains "Go to first" (Home) and "Go to last" (End)
entries, with separators grouping the menu by intent. Absolute jumps; loop navigation does not affect them
(1e8c316c). - Dev-only
screenshot_windowMCP tool that captures the entire native window — overlays, vibrancy, modal
panels — by shelling out to/usr/sbin/screencapture -l <windowID>. Compile-time gated to debug builds; release
builds neither register the tool nor link the dispatch arm. Requires Screen Recording permission on first use
(9954a91d). - Website download buttons now show the file size next to the architecture
(b3b17f46).
Changed
- Markdown / prose formatting unified across the monorepo via
oxfmt(Cmdr's setup). Replaces Prettier; runs
from repo root overdocs/, everyCLAUDE.md, root markdown, and website source. Auto-fixes locally, checks in
CI (a30c1e5d,
cdaa80af,
47742664). - Histogram and EXIF overlay backdrop alpha bumped from 0.55 to 0.66 for legibility against bright images
(6846416a). workflow_dispatchcan now trigger website deploys directly from the Actions tab
(446b8ae0).
Fixed
- Stale
prvw://statesnapshot on background preloads. Found while building loop navigation:poll_preloader
only updated shared state when aReadymatched the pending current index, so neighbor preloads left
cache_indices(and other fields) stale until the next user action. Now updated on every preload arrival
(88b37740). - Histogram briefly showed the previous image's curve on cache-miss navigation while the QuickLook thumbnail
placeholder was on screen.display_thumbnail_placeholdernow clears the cached histogram data so the right
histogram appears once the full image arrives (a6c833c6). - Pricing card on the website was clipping the Download dropdown via
overflow-hidden; the 1px accent line is now
inset within the rounded corners so the parent doesn't have to clip
(c1e467ec).
Prvw v0.11.0
Added
- Blurry thumbnail placeholder while the full image decodes. Cache-miss navigation now uploads the QuickLook thumbnail through the same display pipeline as the full image, so zoom, EDR, and auto-fit match. New
thumbnails/module with a centered-outward scheduler (80fa5102). - Parallel pixel-dimensions prefetcher. A 16-thread worker reads dimensions for every index in
current ± WINDOW_RADIUSupfront, so placeholder display drops from 200 ms – 1.3 s on slow shares to 1 – 2 ms post-warmup. Three-tier dispatcher:image::image_dimensionsfor PNG/GIF/BMP, a single-buffer JPEG parse for one SMB round trip, ImageIO for the rest (d02e61d7). - Updater now surfaces "update available" on no-file launches from Finder / Dock too; download is deferred to viewer init so an admin prompt can't interrupt onboarding (0ee6b337).
Changed
- Navigation no longer freezes on slow network shares. QuickLook submission moved off the main thread to a dedicated
prvw-thumbgenworker (was costing ~150 ms per submit on SMB), completion blocks batch into a shared queue so a 38-thumb burst wakes winit 1 – 2 times instead of 38, andmark_readyno longer pre-reads source dims (~7 s of main-thread blocking on a 38-thumb burst, now lazy) (16d63b46). - Thumbnail scheduler prioritises immediate neighbours first, then outside-window, then current. The previous order left the next-arrow target without a placeholder for ~5 s after launch on slow folders (d02e61d7).
- Updater bundle swap is now atomic via
renamex_np(RENAME_SWAP)— no window where the app is absent or partial, and a crash mid-update leaves one intact bundle either way (0b52f850). - Updater deregisters the old bundle from Launch Services before swapping so the "Open With" menu no longer shows both old and new Prvw side by side after an update (bb0bbbe0).
- Updater only runs on bundles installed in
/Applications; dev builds and~/Downloadscopies are skipped (0ee6b337). - Website tagline + features section refreshed (0664402f); terms and conditions updated (5bae3ce1).
Fixed
- Escape now closes About, Onboarding, and Settings windows again. The hidden Escape button was excluded from
performKeyEquivalent:traversal; switched to borderless + zero-size frame (dc8ee8da). - Integration tests no longer flake from a port-keyed
PRVW_DATA_DIR; eachTestAppnow gets its owntempfile::TempDir(5fd0f0de).
Prvw v0.10.0
Changed
- Navigation is now near-instant in RAW folders. End-to-end rework of the preloader to route the current image
through the background pipeline on a cache miss instead of blocking the main thread, so a fast key press never
freezes the UI. The pool switched from a custom rayon pool to a single dedicatedstd::threadworker — rawler's
internalpar_iterinherits the caller's pool, so a 1-thread custom pool was starving rawler's parallel stages
(demosaic, chroma_nr, sharpen) of cores. A plain OS thread isn't a rayon worker, sopar_iterfalls back to the
global pool and each 20 MP ARW preload now decodes in ~300-450 ms instead of ~2 s. Preload priority is now
direction-aware: forward navigation loadsN+1, N+2, N-1, N-2in that order, backward mirrors, and startup
interleaves. In-flight decodes for indices still wanted are no longer restarted across successiverequest_preload
calls — they keep their cancellation token and continue from where they left off. A 30 ms debounce coalesces wheel
spins: 20 fast clicks collapse into a singlenavigate_by(±20)jump with one decode. Cache entries outsideN±2
get evicted on every navigation (previously they lingered until the LRU budget pushed them out — visible on small
JPEGs where 50+ could fit the 512 MB budget). The wgpuTexturefor the currently-displayed image is now
explicitlydestroy()'d on image swap — fixes a 4 GB+ RSS bloat on long RAW sessions where Metal on unified memory
wasn't returning the backing just by replacing the bind group. File I/O moved to an abandonable detached thread
with channel-based polling, so a wedged network share (SMB, flaky NFS) can't block the caller —std::fs::File::read
has no timeout, so the old in-thread cancellation check did nothing until the kernel unblocked the syscall. - Preloader debug logs are now structured and greppable. Each preload task emits one line on start and one on
finish with the file name, position label (N+1,N-2, ...), 1-based directory position, and total wall time:
Initiated loading 9.jpg (N+1, 10/17)/Fully loaded 9.jpg (N+1, 10/17) in 25ms. Cache evictions log
Evicted 7.jpg (N-3) from memory - 2.5 MB freed (out of window)or... (LRU)depending on the trigger. Enable
withRUST_LOG=prvw::navigation=debug.
Fixed
- Scrolling through a folder of RAWs no longer stutters when decodes can't keep up. The old preloader kept
submitting low-priority neighbor decodes that shared CPU with the currently-needed image, so pressing right six
times at 400 ms intervals pushed the priority-zero decode from ~500 ms to 2-3 s. Navigation now defers the
priority-zero render until the decode lands on the main thread viapoll_preloader, with a clean "Loading…" title
in the meantime. Side effect: no more double-decoding of the same image (main thread + rayon) on cache miss. - Lens correction polarity was doubling distortion instead of correcting it on some lenses. The sign was
reversed inapply_distortion_resampleso barrel correction turned into barrel amplification and pincushion
correction turned into pincushion amplification. Fixed; distortion now goes the right way. - HDR / EDR output now actually renders HDR-bright on XDR displays (Phase 5.2). The original Phase 5.1 path decoded
RAWs into a half-float buffer but routed them throughmoxcmswith a gamma-encodedkCGColorSpaceExtendedDisplayP3
layer, which clipped above-1.0 linear values at ICC-transform time and left the EDR compositor with nothing above
display-white to brighten. Two fixes: (1) on the HDR path only, bypassmoxcmsand apply a direct linear Rec.2020 →
linear Display P3 3×3 matrix (color::profiles::rec2020_to_linear_display_p3_inplace) that preserves above-white
values. (2) Flip theCAMetalLayercolorspace tokCGColorSpaceExtendedLinearDisplayP3so the compositor
interprets the buffer's linear values correctly. SDR path still goes throughmoxcms→ user's display ICC and is
bit-identical to Phase 6.1. Trade-off to name: HDR output uses Display P3 rather than the user's calibrated display
profile, since the OS's EDR compositor takes over colorspace conversion on that path — inherent to going into EDR
mode, not a choice. Seedocs/notes/raw-support-phase5.md.
Added
- RAW per-stage timing + benchmark table (Phase 6.4):
decoding::raw::decodenow emits one DEBUG log line per
pipeline stage plus a comma-separated summary line, ready to grep. Enable withRUST_LOG=prvw::decoding::raw=debug.
apps/desktop/src/decoding/CLAUDE.mdships a reference table of typical warm-decode timings on a 20 MP ARW (M3 Max,
release, defaults) so agents and humans can spot regressions without a profiler. Pairs with a new
Settings → General → "Preload next/prev images" toggle that disables background preloading so single-image
cold-start decodes can be measured without concurrent work interfering. - HDR brightness gain (Phase 5.2): a new 0.5 – 4.0 slider in Settings → RAW → Output (default 2.0) pushes
scene-white content into the EDR headroom so HDR output reads genuinely "HDR-bright" on an XDR / OLED panel instead
of timidly preserving SDR brightness and only using headroom for sparse specular highlights. Matches the visual
brightness of Preview.app / Photos on the same display. Ignored on the SDR path;hdr_gain = 1.0restores the
pre-5.2 HDR behavior (above-white content preserved but no overall brightness lift). - Clarity (local contrast) for RAW (Phase 6.2): RAWs now get a larger-radius unsharp-mask pass on luminance before
capture sharpening. Lifts midtone features — shape silhouettes, textures, the mid-frequency content — that survive
display downscaling, so the image reads crisper at fit-to-window zoom, not just at 100 %. Same math as capture
sharpening, σ ≈ 10 px instead of 0.8 px, luminance-only. New "Clarity (local contrast)" toggle at the top of
Settings → RAW → Detail, with "Clarity radius" (2 – 50 px, default 10 px) and "Clarity amount" (0.00 – 1.00,
default 0.40) sliders directly beneath. On by default; toggling off reproduces pre-6.2 output. Closes the visible
"crispness gap" against Affinity's "Detail Refinement" pass. Adds ~200 – 400 ms to a 20 MP decode on Apple Silicon.
Seedocs/notes/raw-support-phase6.md.
Changed
- DCP ~1.6× faster (Phase 6.5): the per-camera
HueSatMap/LookTabletrilinear-interpolation LUT apply in
color::dcp::applyis now#[multiversion]-annotated (NEON / AVX2+FMA) with branchless HSV conversion,
f32::mul_addon all seven trilinear lerps, andrem_euclidcalls on the hot path replaced with compare-and-add
(the single biggest win —rem_euclid(6.0)inrgb_to_hsvinvokedfmodfper pixel). Processes pixels in
1024-wide rayon chunks to amortise scheduling overhead. ~36 ms → ~22 ms per 20 MP decode (Apple Silicon M3 Max,
release). Applies to bothHueSatMapandLookTablepaths since they shareapply_hue_sat_map. Output is
bit-equivalent to the scalar reference within float rounding (max channel Δ 1.79e-7 across a 256×256 synthetic
buffer; well below the golden regression's < 3.0 ΔE tolerance). - Clarity ~10× faster on 20 MP (Phase 6.4): the σ=10 local-contrast pass now takes ~14 ms instead of ~144 ms on a
20 MP image (Apple Silicon M3 Max, release). For σ ≥ 4 and images ≥ 1 MP, clarity now downsamples the luma plane 4×
(box average), blurs at σ' = σ/4 (~15-tap kernel instead of 61), then bilinearly upsamples — the Gaussian-blurred
signal is low-frequency by design, so the round-trip is near-invisible. Small σ and small images still take the
direct-convolution path for byte-identical output against pre-6.4. Total per-decode budget on a 20 MP HDR ARW drops
from ~650 ms to ~293 ms warm, a ~2.2× overall speedup when paired with the HDR-diagnostic log gating below. See
apps/desktop/src/decoding/CLAUDE.md§ "Per-stage timing" for the reference table. - HDR-diagnostic log scans now run only when info logging is active (Phase 6.4): the three post-pipeline
peak-value scans (peak linear value,peak post-ICC,peak f16) are now gated behindlog::log_enabled!(Info)
and, on the HDR branch, parallelized via rayon. ~40 ms saved per HDR decode when info logging is off; ~30 ms when
it's on. No observable behavior change — release builds that don't ship info logging pay nothing. - RAW defaults tuned against Affinity / Preview (Phase 6.1.1): the parametric RAW stages now ship with values
closer to Affinity Photo's per-camera-tuned output on our sample set —DEFAULT_SATURATION_BOOST0.08 → 0.18,
DEFAULT_MIDTONE_ANCHOR0.40 → 0.45, and a newbaseline_exposure_offsetslider (default +0.73 EV) adds a
user-controllable offset on top of the camera's baseline EV. The Settings → RAW layout co-locates each slider
under its matching toggle instead of the standalone "Tuning" section (saturation slider under the saturation
toggle, midtone slider under the default-tone-curve toggle, etc.) so the tune-by-eye UX is obvious. Flipping a
toggle off continues to reproduce pre-6.1.1 output bit-for-bit on a per-image basis. - Lens correction resampler is now SIMD-accelerated (Phase 6.3): the bilinear resampler inner loops in
apply_distortion_resampleandapply_tca_resampleare compiled for NEON (aarch64) and AVX2+FMA (x86-64)
viamultiversion. The sampler is now branchless (NaN/inf coords handled without a conditional early return)
and usesf32::mul_addfor FMA hints. The TCA path additionally extracts per-channel sampling into
sample_single_channel_bilinear_fast, eliminating 2/3 of the redundant channel computation the original
sample_rgb_bilinearcalls did. Measured per-row speedup on M-series Apple Silicon: distortion ~1.0×
(already memory-bandwidth-bound), TCA ~1.6×. Output is bit-identical within f32 FMA rounding tolerance. - **Capture sharpening now runs o...
Prvw v0.9.0
Added
- Camera RAW support via
rawler: open DNG, CR2, CR3, NEF, ARW, ORF, RAF, RW2, PEF, and SRW files. Decode pipeline
includes black/white level correction, PPG demosaic for Bayer sensors, bilinear for Fuji X-Trans, white balance,
camera color matrix with Bradford chromatic adaptation, and sRGB gamma. NEON SIMD on Apple Silicon, rayon
parallelism. Orientation is pulled from EXIF metadata sincerawlerhard-codesRawImage.orientation. Known limits
in this first pass: no DNGOpcodeListinterpretation (iPhone ProRAW gain maps), no DCP profiles, X-Trans demosaic
is bilinear. Seedocs/notes/raw-support-phase1.mdfor design decisions and the Phase 2/3 outlook
(b4bc775) - File associations for all 10 RAW formats: Finder now recognizes Prvw as a handler for DNG, CR2, CR3, NEF, ARW, ORF,
RAF, RW2, PEF, and SRW via their standard Apple UTIs.Info.plistcarries all 16 document types now (6 standard + 10
RAW) - Settings > File associations: redesigned into two sections, "Standard image formats" (JPEG, PNG, GIF, WebP, BMP,
TIFF) and "Camera RAW formats" (DNG, CR2 + CR3, NEF, ARW, ORF, RAF, RW2, PEF, SRW) with vendor labels. Each section
has a master "Set all" toggle with tri-state support: when some formats are on and others off, the master shows a
"Mixed" indicator; clicking mixed or off sets all on, clicking on sets all off. Per-format small toggles keep fine
control
Changed
- Onboarding window: redesigned into a four-step checklist (Install Prvw.app, Set as default viewer, Move to
/Applications, Open images). Each step uses a custom green checkmark (dimmed for pending steps) rendered at runtime
from the source SVG path viaNSBezierPath. Step 2 holds the "Set Prvw as the default viewer for all of these
files" button and shows a natural-language sentence summarizing which app currently opens each of the 16 supported
image formats. Step 3 checks whether the binary is in/Applications/and shows a drag hint when it isn't. Step 4's
copy adapts to step 2's state: "double-click any image" once Prvw is the default, or "right-click any image and
choose Open with → Prvw" beforehand. Content is left-aligned, the window is wider and taller to give the checklist
breathing room, and the title drops thevprefix ("Prvw 0.8.0") - Decoding module: single-file
decoding.rssplit into adecoding/module with per-backend files (jpeg.rs,
generic.rs,raw.rs) plus shareddispatch.rsandorientation.rs. Public API unchanged
(b4bc775) - CI: macOS-only modules (AppKit settings panels, color transform tests) gated behind
#[cfg(target_os = "macos")]
so cross-platform builds compile cleanly. Groundwork for Windows and Linux support later
(e9b5de4,
3f00979,
815b727,
96218dd)
Fixed
apply_orientationunderflowed on zero-width or zero-height input for EXIF orientation 2. Now early-returns
(b4bc775)- Restored per-row handler transparency in Settings > File associations: each format row again shows which app currently
handles it, or which app handled it before Prvw took over. Covers all 16 formats (6 standard + 10 RAW)
Prvw v0.8.0
Added
- Settings window: new sidebar layout with General, Zoom, Color, and File associations sections. Cross-dependencies
disable dependent toggles automatically (ICC off → Color match / Relative colorimetric disabled; Auto-fit on →
Enlarge disabled) (dc43505,
0dd4849) - File associations panel: per-UTI toggles, "Set all" master toggle, 1-second polling of handler state, previous
handler rollback when you turn a toggle off (0dd4849,
17b76a3) - Rendering intent toggle (View menu + Settings > Color, Cmd+Shift+R). Default is perceptual; toggle to relative
colorimetric. Disabled when ICC color management is off. Persisted asuse_relative_colorimetric
(b42814f) - Scroll-to-zoom toggle in Settings > General (off by default). When off, scroll navigates between images (down = next,
up = prev). Cmd+scroll always zooms regardless of the setting
(d55b7e9) - Pinch-to-zoom on trackpad, cursor-centered. Works with auto-fit window resize, same as scroll zoom
(ef8d0bf) - Keyboard shortcuts for zoom: Cmd+= (zoom in), Cmd+- (zoom out), Cmd+0 (actual size)
(ec2aba4) - Title bar toggle in Settings > General (on by default): reserves a 32px strip at the top so the filename and zoom
pills don't cover the image. Screenshot-friendly when off
(64e0d87) - Title bar vibrancy: Liquid Glass on macOS 26, classic frosted glass on older versions. The area around the image
(when the image doesn't fill the window) gets a darker HUD-style vibrancy. Fullscreen switches both to opaque black
so screenshots and projector-style viewing aren't distracted by the desktop blurring through
(7eede14) - Integration test suite (17 tests): Settings open/close/switch, zoom in/out, fit/actual, auto-fit toggle, navigate,
refresh, window geometry. Each test spawns its own app instance on a dynamic port
(0dd4849)
Changed
- Source layout: flat
src/with infrastructure (app/,render/,platform/) and features (about,color,
decoding,navigation,onboarding,qa,settings,window,zoom, …) as siblings. Each feature owns its
runtime state via aStatestruct onAppinstead of ~20 flat fields. No behavior change
(27eca5e,
e88027b)
Fixed
- Closing the onboarding window now quits the app. Previously, a no-file launch (Dock or
cargo runwith no args)
left the event loop running with nothing visible after the user clicked Close, because the onboarding is a raw
AppKit window and doesn't generate a winit close event
(e81bbdf) - CGColor / CGColorSpace encoding crashes in
setColorspace:(display profile) and the Settings separator:
msg_send!encoded these as^vinstead of^{CGColorSpace=}/^{CGColor=}. Fix uses rawobjc_msgSendto
bypass the type check (17b76a3)
Prvw v0.7.0
Added
- ICC color management: embedded source profiles (JPEG, PNG, TIFF, WebP) are transformed to accurate output colors.
Level 1 converts to sRGB, Level 2 targets the actual display profile via CoreGraphics FFI
(CGDisplayCopyColorSpace). Images without profiles assumed sRGB. Display changes flush the cache and re-decode
(ee226ac,
94820a8) - View menu toggles: "ICC color management" (Cmd+Shift+I) and "Color match display" (Cmd+Shift+C), both persisted in
settings. Disabling ICC grays out color matching (L2 depends on L1)
(a088330,
b952b64)
Changed
- ICC engine: replaced lcms2 (C bindings) with moxcms (pure Rust, NEON SIMD). 24MP transform: 247ms -> 45ms on M3 Max.
No C toolchain needed for cross-compilation
(f568b18)
Fixed
Prvw v0.6.3
Fixed
- Finder double-click file opening: replaced
NSAppleEventManagerhandler (overridden by AppKit's
NSDocumentController) with ObjC runtime method injection (class_addMethod) that adds
application:openURLs:directly to winit'sWinitApplicationDelegateclass
(9417ab0)
Changed
- Zoom model uses logical pixels: zoom=1.0 = 100% = one image pixel per logical pixel. The overlay
correctly shows 100% for naturally-sized images on Retina displays (was 200%) - Compiler-enforced
Logical<T>/Physical<T>newtypes prevent mixing logical and physical pixel
values. Winit interop viafrom_logical_size,to_logical_pos, etc. - Removed 329 lines of dead modal onboarding code
Prvw v0.6.2
Fixed
- Finder double-click "cannot open files in JPEG Image format":
CFBundleTypeRolewas missing fromInfo.plist
document type entries. macOS requires this to know the app can actually open files, not just be registered as a handler - CI: add
libxdo-devto Linux apt-get formudadependency - Auto-updater: call
lsregister -fafter replacing the.appbundle so macOS picks up new document types in future
updates
Prvw v0.6.1
Fixed
- Finder double-click now works: macOS sends file paths via Apple Events (not CLI args), but the app was exiting before
the event loop started. Now the event loop always runs, with a 500ms wait for Apple Events before showing onboarding
(f6e0fef)
Changed
- Onboarding window is now non-modal (doesn't block the event loop), allowing Apple Events and QA commands to arrive
while it's showing - Code refactors:
scale_factorstored on App,TextBlockbuilder pattern,MonitorBoundshelper,LogicalF64/
LogicalF32type aliases for coordinate clarity