Lightroom Classic's keyboard shortcut page lists Command+K (Ctrl+K on
Windows) for Add keywords. LightCraft had no key for it.
Add Keywords (panel.addKeywords, Cmd+K) shows the Keywords panel,
opens its Keywording section when it is folded shut, and puts the
cursor in the Add keyword box, so the next keywords typed go to the
selected photos. Pressed again, it leaves the panel open. With no photo
selected the panel shows its note, and selecting photos later does not
move the cursor into the box.
The command label has a row in every catalog. The KEYC-METADATA row,
docs/ui-parity.md and docs/library-shortcuts.md list the key. The
KEYC-METADATA row also lists the existing Alt+1 to Alt+9 keyword set
keys in place of "no keyword sets".
Resolves the parity summary (regenerated), drops a duplicated #[cfg(test)], files the What's New entry
under today's date and bumps the checklist's Last updated line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main replaced "Show Clipping" with "Clipping": the PR's Beschneidung term moves to the new key.
Also takes CodeRabbit's two wording suggestions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A pass over all 2,565 interface strings and 299 formats messages of the
German catalogs (What's New lines not included). 108 messages change:
- Lightroom terms: Tönung (Tint), Beschneidung (Clipping)
- one word per thing: Bedienfeld (panel; Bereich stays for sidebar
sections), Begleitdatei (sidecar), Dia (slide), Zuschnitt (crop),
Farbmarkierung (label) where "Markierung" read as the pick/reject flag,
Verschlusszeit, Smart-Album, Perspektivkorrektur (Upright),
Aktivitätsstapel
- "Ihre Dateien/Fotos" at the start of a sentence read as the formal
"your" in a du catalog; rephrased
- double colons and "1 Fotos" in count messages, awkward phrasings
- spaced en dash instead of em dash, no space before a final ellipsis,
German quotes around {profile}
The terms are listed in docs/localization-de.md; localization-parity
records the review.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CodeRabbit on #2689 (head 496291276):
- `cull` takes the view it hands on from before ending an open drag, so the
drag's own commit can't change it first. (In practice the view already
follows the drag's preview, so the photo is out of it mid-drag either way;
this is the order every caller should use.)
- LR-BEHAV-LOCALE cites the commands of all 18 languages.
- Two 2026-10-21 revision rows moved above the 2026-10-20 ones.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CodeRabbit on #2689 (head 496291276):
- docs/control-protocol.md, `app.recentLibraries`'s doc, `is_missing`'s
doc: a folder that can't be listed is `exists: null` (couldn't tell), not
`false`, since 142e496f5; the launch window still shows why for every
reason.
- a_failed_lookup_doesnt_mark_a_recent_library_missing skips (System) when a
folder with no permissions can still be read, as when running as root.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Its code is already in the batch; docs keep the batch's version, headers list
#2468's change entries too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* security: harden dev-tool scripts and the testkit skip-log test (CodeRabbit)
- tools/install-sam3.sh: the HF bearer token went through curl's argv, where
`ps` exposes it to other local accounts (CWE-214); it now travels through
a private 0600 temp config file removed on exit.
- packaging/macos/import-cert.sh: the decoded .p12 was written to a
predictable path in a shared TMPDIR, where a precreated attacker-readable
file would receive it (CWE-377); both cert and keychain are created inside
a mktemp -d 0700 directory now.
- docs/showcase/run.py: the reply reader copied whole responses with no size
cap (obvious quadratic-copy and unbounded-memory DoS). It accumulates in a
bytearray now and closes the connection past 128 MiB.
- tools/spectral_camera_table.py: alias/make/model strings and comments from
the rawtoaces-data checkout moved directly into Rust source; one newline in
a comment became Rust code. External text is now escaped as Rust literal
content and comments are normalized to one printable line.
- crates/testkit: appended_lines_accumulate reused a pip-predictable temp
dir by creating it with create_dir_all and carried on after failing to
remove its log path, so the append would follow an attacker-planted
symlink (CWE-59); it now fails the test if the directory exists instead of
testing through it, and removes any existing log path only after clearly
checking it, always failing the test instead of proceeding when removal fails.
Findings #10080/#10078 PR (rows 2, 6, 8, 9, 15 of the
storytold-lightcraft-agent-findings report).
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
* security: bounded catalog keyword, repair, journal recovery work (CodeRabbit)
- keywords: from_file searched every hierarchical level for each flat keyword
and `same` lower-cased two strings per comparison — one lowered level set
instead (CWE-407, row 97).
- keywords: parse_keyword_list scanned each keyword's growing synonym list per
line — a lowered-synonym set per keyword instead (CWE-407, row 100).
- keywords: keyword_tree stored every level's prefix twice, so a photo XMP
keyword nested deeper than a list file accepts made stored bytes quadratic
in the level count; the tree caps at MAX_LEVELS, and keyword_list_text names
whatever lies deeper from the photos' and list's own spellings, so the file
still reads back (CWE-400, row 103).
- catalog: repair validated a folder chain once per album — memoized by chain
now, so a valid chain in a flat snapshot costs one walk overall (CWE-407,
row 101).
- store: journal append and truncate follow a planted catalog.log symlink;
both refuse any non-regular file at the name before writing through it
(CWE-59, row 107), like the staging writes since #1502.
- journal: resync re-validated the whole remaining suffix UTF-8 for every
candidate record start; a whole line is validated once (covering every
candidate in it), and an invalid line's re-checks stop at a 64 MiB budget,
with the tail then staying a torn one the damaged path keeps and reports
(CWE-407, row 109).
New tests: from_file case-insensitive level match, synonyms kept once, a
4096-level keyword cut at MAX_LEVELS, a link at catalog.log refused, and
resync over a fragment of 1024 record starts.
Findings rows 97, 100, 101, 103, 107, 109 of the
storytold-lightcraft-agent-findings report.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
---------
Co-authored-by: Codebuff <noreply@codebuff.com>
CodeRabbit on #2689:
- `unavailable` used `exists()` / `is_dir()`, which are false for any
error: a parent folder that can't be read or a share that timed out came
out as "Not found" and greyed the entry out. Only an absent path is "not
found" now; other lookup errors are "This library couldn't be checked:
<reason>" (LookupFailed), and a folder that refuses to be listed is no
longer counted as missing either (it is there: permissions, macOS
privacy), so neither is greyed out and opening says why.
- Every recent library was checked again every 30 s while the app ran,
waking external drives and network shares. A library not found is looked
for again every 30 s (a drive plugged back in; an absent path wakes
nothing); the others only when the window comes back to the front, for
the 8 entries shown; paths never checked are checked once.
Tests a_failed_lookup_doesnt_mark_a_recent_library_missing and
recent_libraries_are_checked_again_only_when_the_window_comes_back fail with
the old lookup / the old timer / no timer for libraries not found.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CodeRabbit on #2689 (head 02743d187):
- crates/engine/src/cmd/library.rs: `cull` (#2327: a rating, flag or label
that takes the active photo out of the view) and `photo.delete` /
`photo.deletePermanently` moved the active photo without ending an open
interaction, unlike `step` and `library.select`: the drag stayed open with
no undo step. Each ends it now before its own change, so the drag is its
own undo step and comes first: undo brings the rating or the photo back,
with its edit, then the edit (Delete Permanently's undo restores the
edit too). Test an_open_edit_ends_before_the_selection_moves_on (rating,
delete, delete permanently) fails without the fix.
- docs/parity-checklist.md (LR-BEHAV-LOCALIZE) and docs/localization.md: list
every shipped language (Polish, and the others the row lacked; Italian in
the localization doc).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
library.source cleared the filter's library folder when a library folder became the source, but not the filter's album when an album did. LibrarySource::to_filter makes the album shown replace the filter's, so the grid was right; but the filter bar has no album chip, so the old album stayed in the filter unseen and narrowed the next source (All Photos, Picks). As for a library folder, the one shown now wins.
crates/ui-egui/src/lib.rs: the batch's module list (tests_capture_time) with
#1922's. LR-VIEW-ZOOM: the batch's row plus #1922's middle-button sentence.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Docs only, and the batch's side already holds everything #2701's merge
resolved (its rows that differ are main's rows other batch PRs edited).
Tracker summary recomputed (`cargo xtask parity --write`).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/parity-checklist.md: header lists both sides' changes; #1499's two new
dated prose entries (2026-10-23, 2026-10-24) become revision rows like its
two earlier ones, so the history stays one table.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts:
- apps/lightcraft/src/native_menu.rs: upstream's item swapping (macOS keys
given up for real, Help menu not swapped, Settings / Quit tracked) with
KEY-GRID's keymap-aware rows. `Item.label` stays the text the row shows
(native_item_presentation), so `Item::native_label` reuses it instead of
formatting it again; Settings / Quit store theirs through
display_item_label, as upstream's native_label did. No early return after
a rebuild (upstream).
- crates/ui-egui/src/i18n.rs: upstream's tests (#2729: one test per check
across languages, gaps reported, never a failure) plus the batch's two new
ones (dropped-photos notice, Polish counts); production code as merged.
- grid.rs, keyword_list.rs, masking.rs: the translation tests #2729 removed
stay removed; the batch's new keyword and segmenter tests stay.
- cmd/library.rs, ui-egui/src/lib.rs: both test modules, each with its
#[cfg(test)].
- xtask/src/main.rs: #2302's test_selected and upstream's translation_gaps.
- docs: tracker / gap rows take both sides' edits, headers list both sides'
changes, history rows of both sides newest-first. What's New: one section
per date, newest first (the batch had 10-22 above 10-23, and second 10-21
and 10-19 sections); no line lost.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In Detail, dragging with the middle button (the mouse wheel pressed) pans the
zoomed photo with a hand cursor, in every tool. Remove / Heal / Clone, the
mask brushes and Crop no longer have to be left to look around a zoomed photo,
and they don't paint or move during that drag: it is handled with the pinch and
scroll gestures, before the tool overlays.
Right-click keeps the photo's context menu and Space keeps the zoom toggle.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lightroom Classic's histogram is a control surface: its plot is split into five
tonal zones and each one moves the Basic control it names. The Edit panel's
histogram now does the same, wired through the engine like every slider.
- Zones: Blacks 0.00-0.15, Shadows 0.15-0.35, Exposure 0.35-0.65, Highlights
0.65-0.85 and Whites 0.85-1.00 of the plot's width, each naming the develop
control it adjusts (light.blacks ... light.whites), so its range, default and
steps are the panel's own control specs -- no copy of them.
- The zone the press landed in stays locked for the whole drag (the new
Gesture::HistogramDrag), so the pointer may cross the others without switching
control. The strip under the plot (the readout and the EXIF line) and the
clipping buttons in the plot's top corners are not zones.
- A sideways drag dispatches develop.set between develop.beginInteraction and
develop.endInteraction -- the sliders' own path -- so the draft render follows
the drag live, nothing is committed while it goes on, and the release lands the
whole drag as one undo step. Travel of the plot's width sweeps the control's
whole range (Shift a tenth, as the sliders' fine drag) and clamps to its spec;
the unrounded value is carried from frame to frame so a slow drag adds up
rather than rounding back to the step it already has, and the first reported
frame takes the whole travel from the press, since egui withholds a drag until
the pointer has left its click threshold.
- Hovering a zone tints it and names it with its value in the strip under the
plot ("Exposure: +0.45 EV"), replacing the EXIF line, and the pointer shows the
sideways-resize cursor. A double-click in a zone puts that control back to its
default in one undo step.
Tests: the zone mapping at every boundary, the tiling and the controls' specs,
the drag/rounding/clamping arithmetic (hostile input included) and the readout
wording; end to end through the control channel: a drag per zone, the zone lock
across the plot, the Shift ratio, clamping at both ends, live dispatch with one
undo step on the release, the double-click reset, the hover highlight, readout
and cursor, and a drag on the readout strip or clipping chevrons changing nothing.
Docs: the tracker's LRC-DEV-HISTOGRAM-DRAG row added and LR-VIEW-HISTOGRAM's
"no drag-to-adjust" note replaced, with the summary regenerated by
`cargo xtask parity --write`; a dated entry in docs/whats-new.md.
Sources: own design; the zone layout and Lightroom Classic's histogram-region
behaviour as the reference (observed read-only).
Co-Authored-By: deepseek-flash <noreply@deepseek.com>
A message a catalog lacks, or translates as an empty string, shows in
English and fails nothing:
- build.rs no longer requires every formats catalog to carry the same
messages, and tr_format! gets a last arm that formats the English
source, so a message no catalog has builds.
- Coverage tests (every language translates X) and tests of one
language's exact wording are gone; the language-switching, layout,
date, placeholder and user-value tests read the catalog's own value
instead of a hard-coded translation.
- i18n::tests::translation_gaps_report lists, per language, the plain
and format messages it lacks and its untranslated What's New lines.
It always passes; cargo xtask ci
runs it from the test step's binary after the tests.
- catalogs_load checks that every catalog file is a JSON object of
strings with each key once; it.json named "Dimensions" twice.
dialog_field took the focus back every frame while the field wasn't being edited, so Tab never
reached the check box, Cancel or OK of New Album, New Smart Album, Save Metadata Preset and the
other one-field dialogs. It now takes the focus once, when the dialog opens (the way the keyword
dialogs already did).
crates/ui-egui/src/lib.rs: the batch's handle_dropped_files refuses presets
while no library is open, as #2701 does. The drop card says so, and its
"photos wait" line is desktop-only (the web build's drops import into its
session as before). photos_dropped_with_no_library_open_wait_for_one also
drops a preset; LR-IMP-DRAGDROP row updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Presets belong to a library (presets.json, <library>/Profiles): dropped
while the launch list or the failed-library window is up, they went into
the session the library opened then replaced. They are refused with a
toast now; the photos of the same drop still wait for the library. Docs:
What's New, control protocol, the #1290 tracker row.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
crates/ui-egui/src/lib.rs: the batch's handle_dropped_files holds the photos
(library_problem::hold_dropped) before the Lightroom / running-import
checks. The drop card (drop_overlay.rs, LR-IMP-DRAGDROP) says the photos
wait instead of announcing an import or a review; test
photos_dropped_with_no_library_open_wait_for_one drops through
`ui.dropFiles`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>