Scrolled down a long Keyword List, ticking or unticking a keyword grew or
shrank the Keywords box above it (a row of chips, suggestions) and slid
the tree under the pointer. The panel now moves its scroll offset by the
same amount and draws the pass again, so the list stays put. It holds
only while the box's top is scrolled out of sight and the change does not
come from the box itself (the pointer on it, or typing in its field).
The offset is set on the scroll area as the redrawn pass starts, from the
offset the pass before started at: a scroll asked for during a pass lands
a frame later, and egui pulls a shrunken panel's offset back by the end
of a pass. Ticks clicked one after another (a script, a quick hand) then
land on the rows clicked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in batch 35 / v0.8.0. Code merged cleanly; conflicts only in docs: the LR-MERGE-HDR row keeps main's
lens-correction text plus this branch's white-balance item, and this branch's What's New entry moves to the
2026-10-14 section.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015gRtdWF1E9358FfzhzYqCS
Brings in #1423 (HDR merges keep the lens corrections), which landed in batch 35 / v0.8.0. hdr.rs merged cleanly:
the result's lens corrections come from the reference frame after the brightness ordering. Conflicts only in docs:
the LR-MERGE-HDR row keeps main's lens-correction text plus this branch's two items, and this branch's What's New
entry moves to the 2026-10-14 section.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015gRtdWF1E9358FfzhzYqCS
photo.allMetadata listed no lens for a RW2: Panasonic writes no Exif
LensModel, and the maker note that names the lens is in the Exif of
the embedded JpgFromRaw. List that maker note tag (LensType, 0x0051)
as a row of a Panasonic group, the same name photo.inspect shows, for
a RW2/RWL and for a JPEG with the note in its own Exif.
Checked on the CC0 sample DMC-G80 P1000403.RW2: Panasonic / Lens Type
= SIGMA 60mm F2.8 DN.
LightCraft's RPM requires Fedora package names: `libxkbcommon`, `(vulkan-loader or libglvnd-egl)`.
openSUSE has no `libxkbcommon`, `vulkan-loader` or `libglvnd-egl` package, so zypper refuses it with
`nothing provides 'libxkbcommon'` (exit 4, which the ArtCraft Launcher shows as "error 4";
storytold/craft-launcher#341).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
import_reads_files_off_the_ui_thread_and_can_be_cancelled: the probe was
a 150 ms sleep judged by a 300 ms wall-clock frame budget, which the
loaded FreeBSD QEMU VM can miss. It now waits on the sleeping-NAS gate of
tests_offline, which can also let a counted number of reads through:
frames must go on while every read is held (a watchdog records a frame
that waited), one batch is let through and joins the catalog, then
Cancel and wake: the files being read finish and nothing new starts.
compare_at_one_to_one_gets_a_sharp_window_per_pane ("both originals stay
decoded"): the media cache takes its budget from the process-wide memory
budget, which settings_dialog_by_keyboard_and_clicks sets to 1 GB for a
moment (and left there when it failed on FreeBSD). That leaves a 512 MiB
share, too small for two 24 MP originals (288 MB each), so one pane's
was evicted. compare_at now gives its session the default 768 MiB share;
LIGHTCRAFT_MEMORY_MB=1024 reproduces the failure without this.
dropping_the_lock_frees_the_library_while_another_thread_spawns_processes:
"the spawning thread never got a child off the ground" when a spawn took
longer than the 2 s of lock cycles. The cycles now go on until at least
5 children were spawned during them (at most a minute).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deflate-compressed DNG tiles are written at zlib level 1 instead of 6. Measured on
Sony A7R VI files (67 MP), 4 threads, best of 3:
- Photo Merge DNG (half floats, 3 channels): 3.88 → 0.55 s, 265.7 → 274.8 MB (+3.4 %)
- raw export with Deflate (16-bit CFA): 0.89 → 0.18 s, 96.7 → 103.0 MB (+6.5 %)
- rendered DNG: 4.60 → 0.69 s, 239.0 → 267.1 MB (+11.8 %)
Lossless either way; both written files decode back bit-exactly. Lossless-JPEG
DNGs (the default for convert/import) are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep the HDR histogram and SDR/HDR legend, virtualized sidebar rows and current preset grouping while retaining shared chevrons and inset interactive clipping buttons. Apply shared submenu arrows to the new album import menus. Drop the earlier inpainting compatibility change now present upstream.
Validation: formatting, strict native Clippy and HEIF checks passed; 811 compiled UI tests passed; native SDR and HDR screenshots checked. Full workspace CI is blocked by all_masks_overlay_shows_every_visible_mask_at_once, reproduced unchanged on clean upstream b3d392b4. Publish this prepared rebase at the contributor’s explicit request with that baseline failure documented.
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
The connect-mode MCP client accepted any newline-less or wrong-id reply as
the answer to its request. Require a complete newline-terminated reply of
at most 64 MiB, the matching integer id, boolean ok and a result or string
error; otherwise close the connection and report an unknown outcome without
resending. Idless pre-dispatch refusals from the control server still work.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
settings_dialog_by_keyboard_and_clicks: the Performance tab's GPU status
hint reads process-wide GPU state, which other tests flip through
app.gpu. Without a GPU driver (the FreeBSD VM) the reason is a long wgpu
message that wraps to two lines, while "disabled by the GPU rendering
preference" is one, so the controls below moved by 20 px between the
frame that located "button:settingsMemory-2" and the frame that clicked
it. The status is now one truncated line with the full text on hover;
gpu_status_never_moves_the_performance_controls checks the buttons stay
put (it fails without the fix when no Vulkan driver loads).
offline_originals_never_block_a_frame: the slow NAS probe was a 400 ms
sleep judged by a 300 ms wall-clock frame budget, which a loaded QEMU VM
missed (1.06 s). The probe now waits on a gate the test opens; frames
must finish while it is closed (a watchdog records any frame that had to
wait), so no timing budget is needed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds an option under Settings > Import > Cameras and Cards that opens Import from Device when a camera or memory card shows up. It's off by default, and a card that was already in at startup doesn't ask.
The thumbnail cache wrote .jpg files into the library's thumbs folder, which is usually inside
Pictures: photo galleries listed every thumbnail as a photo (issues #610, #1066), and every read
brought the file's date forward, so each launch made them all look new.
- Cache files are named <32 hex>.lcthumb (JPEG data as before). A .jpg of before is taken over
under the new name when it is asked for; the rest when the cache is tidied, which the engine does
once per cache folder, on a thread of its own with a row in the activity stack that shows its
progress and can stop it. Nothing is rendered again.
- Reading writes nothing: what was read is remembered for the session (a full cache lets it go
last), and only a file that went unused for a week has its date brought forward.
- A folder that holds nothing but the cache gets a CACHEDIR.TAG, when the cache makes it and when
it is tidied; one that also holds somebody's files is not tagged (issue #98).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The desktop app opened at 1600 x 1000 every time: a resize, or a zoom by a
double-click on the top bar, was forgotten at quit.
The size the window rests at and whether it is zoomed (macOS) / maximized are
kept in ui.json (`window: {size, zoomed}`) and the window is built from them.
A zoomed window keeps the size it goes back to. A size counts once the window
has rested at it for half a second, so the sizes a resize or the zoom
animation passes through do not; one given just before quitting is taken at
exit. Full screen and the window's place on the desktop are not remembered.
Windows and Linux say whether a window is maximized. On macOS egui-winit only
refreshes that at launch and when the app zooms the window itself, so there
the window's size is gone by as well: zoomed when the system said so or the
window was seen to come to fill its screen, and for as long as it keeps that
size. Whatever is in doubt is a window of the size it has, which opens as
large as it was left.
A `window` in the settings that is not one reads as none and costs no other
setting.
Run on macOS with one display; the other platforms follow from reading winit
and egui-winit, so the tracker row is partial.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- An offer taken asked the box to start a new edit on the next frame; a
box not drawn then (its panel switched away at once) took the cursor
whenever it was next drawn. The request holds for one frame.
- Return on the frame the box scrolled out of sight still took the
highlighted offer, which was no longer drawn: it gives nothing.
- In a window with room for the offers on neither side of the box they
covered it: they go to the side with more room and show as many lines
as fit there, the highlighted one among them.
- Ukrainian says "suggestions" as the panel does; four clearer wordings
(uk, ru, de, ja).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- "+N more" was for the whole session: it is for the selection it was
asked of, and another selection's long list is short again.
- A keyword two photos spell differently was offered twice under one
widget id, and a click on it gave nothing: it is offered once.
- The offers followed the box out of the panel when it scrolled away and
covered it near the bottom of the window: they are not drawn while the
box is out of sight, and go above it where there is no room below.
- The highlight was a line number, so a change of the library moved it to
another keyword, which Return then gave: it stays with its keyword.
- Esc after an offer was taken brought back keywords just added; Shift or
Alt with the arrow keys moved the highlight instead of the text cursor;
a refused add lost the typed text without a word.
- A name the library lacks was looked up in the library twice per change
of the text: once.
- A wrong value under keywordChips or foldedSections discarded the whole
settings file: it is read as the default.
- Groups follow the tree ("Places > Italy" right after "Places"); a heading cut
short says its whole name on hover; the number of chips measured for a
shortened list follows the box's width; the selection is asked for once
a frame.
- The nine new plain messages are translated in every language.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With several photos selected the box says how many its keywords are of.
A chip's x is a target as tall as the chip and 22 pt wide (it was 14 pt
square) that lights up on its own, and the chip is 4 pt narrower for it.
Taking a keyword off several photos, by the x or the chip's menu, says
which keyword left how many photos and that Edit > Undo puts it back; a
refused removal says why. Chips and their x show the keyboard focus, and
the Add keyword box says in its tooltip that commas separate keywords.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What matches the text being typed (after the last comma) is offered right
under the box instead of in the Suggestions block further down: Down / Up
move through the offers, Return or a click gives the highlighted one (with
the keywords typed before it) and leaves the cursor in the box. With none
highlighted, Return gives what was typed, as before.
A name the library doesn't have is announced as a new keyword, with the
keyword it would go inside, before Return creates it.
The offers have a cache of their own: they and the suggestions are both
asked for on every frame of typing, and each looks through the library's
keywords.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The titles of Keywords and of the Keyword List fold their sections shut
(kept in the UI state as foldedSections), so the list of every keyword is
reachable however many keywords the selection has.
Settings > Interface > Keywords of the selection > Grouped by parent
(off by default): keywords inside the same keyword share a line naming
it, their chips say only what is below it and sit side by side, a list
of more than ten lines stops after eight with "+N more", and the
suggestions say the keyword's own name.
A selection without keywords says "No keywords yet", as the other two
views of the box do.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The left panel shows a count beside every album. Each catalog revision had
every album counted again, and a smart album's count asks every photo of
the library whether its rules hold: one pass over the library for each smart
album after each keyword, rating or slider tick (about 25 ms an album in a
library of 67,000 photos).
`AlbumTally` (`crates/catalog/src/tally.rs`) keeps the counts and, for each
smart album, which photos it holds. After a small edit each smart album asks
only the photos the edit touched (`Catalog::touched_since`); an album of
chosen photos is counted again only when a photo came, went or was deleted.
Counted from every photo as before: new rules, a change to an album, a stack
or the keyword list, an edit to more than 256 photos, another library, and
each new minute while an album asks about "the last N days".
What an album remembers is in proportion to the photos it holds: a bit a
photo id while ids are close together, a set when they are far apart.
The frame after a keyword in that library, both side panels open: about
54 ms -> 27 ms (156 ms before #1660).
Tests: 900 random edits against eight smart albums (keywords, flags,
ratings, labels, sliders, capture dates, file names, an album of chosen
photos) and an ordinary one, the followed counts compared with
`album_count` after each; a delete followed, three hundred photos counted
whole; the members' bits at the edges and for ids far apart.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Everything derived from the catalog was keyed on its revision, which every
op raises. So each edit (a keyword, a rating, every tick of a slider) had
the grid's list queried and sorted again from every photo, the source
counted by a second full query, and the side panels tally the whole library
again. In a library of 67,000 photos the frame after one keyword took about
156 ms with both side panels open (an idle frame: 2 ms).
The catalog now remembers which photos its latest ops touched and what of
them they changed (`Catalog::touched_since`, `aspects_since`, `Aspects`). It
answers "not known" for an op that changes more than photos of its own (an
album, a stack, the keyword list), for changes too long ago, and for a
revision it did not make itself (a library just loaded).
- The view (`Session::visible`): when the same view is asked again after a
small edit, the photos the edit touched are taken out of the list it had,
asked about again, and put back at their place in the order
(`Catalog::follow`). 37 ms -> 0.6 ms. Worked out whole as before: more
than 256 photos touched, a change that is not photos' own, an album in its
own order, Recently Added, Missing Photos, and a view that asks about "the
last N days" (it changes with the clock too).
- The source's count: taken from the list when no filter is set, else
counted without sorting (`Catalog::count`). 37 ms -> 0.
- The side panels (`Caches`): the folder tree, keyword tree, date groups,
people's names and library counts are kept through an edit that changed
nothing they read. A rating or a slider changes none; a keyword leaves the
folders, dates and counts.
The frame after a keyword: about 156 ms -> 54 ms with both side panels
open, 71 ms -> 2 ms with the grid alone (`tests_view_cost.rs`, a synthetic
library, run with --ignored --nocapture).
Still paid on each edit: the count beside every album and smart album in the
left panel (about 25 ms for a smart album there), and the filter bar's lists.
Tests: 1,440 random edits of every photo-local kind across sources, sorts,
filters and the hold, each compared with the list worked out whole; 600
random edits comparing every kept tally with a fresh one; the clock; the
record itself, with its failure paths. `ui.inspect` -> `perf` reports
`viewFollowed` and `viewWorkedOut`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The scan asked the disk about every library photo in the folder: is its
file there, and has it a sidecar under any of four names. On a network
share each question is a round trip, and the scan had just listed the
folder. Measured on a hard disk over SMB, for unchanged folders of 1,600
to 2,400 photos: 4.6 to 7.1 s of questions after a listing of 4 to 5 s.
- The walk hands each folder's listing over (`walk::files_in_noting`:
every entry's name and kind, hidden ones too, and whether that is all of
them). The scan walks the folder once, then checks the photos beside
the probing of new files.
- `sync::Listing` answers "is it there" / "is it a file" where that holds
on any file system: a name the folder lists is there; a plain ASCII name
is not there when the folder was listed to its end, all its names are
ASCII and none is that name in another case. Everything else is asked
of the disk as before (a link, a name Windows reads another way, a
folder that was not walked, and every name not listed in a folder that
holds a name outside ASCII).
Now under 0.05 s for those folders. Two things differ on purpose: a file
the folder lists is there though it can't be opened by its path (a folder
that may be listed but not searched, an over-long path), where such
photos were listed as missing; and the answers are the folder as the
walk saw it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>