#308 painted an opaque label-coloured bar over the bottom 24 px of every
labelled photo in Photo Grid, hiding part of the photo, while its docs
said the photo pixels stay as they are. The footer is now translucent
over the photo and the docs say so. Also adds the one string pt-br lacked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01912WaK6Rmew3wxZuPyX7E8
#258 left the Tint row at 🟡 and docs/tint-direction.md as a review note.
The sign now matches the slider's track, the DNG specification and
Lightroom XMP, so the row is back to ✅; the doc says what happens to
saved custom tints (they shift the other way; nothing is rewritten) and
What's new tells users.
Co-Authored-By: Hanmo <letsgo@88.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01912WaK6Rmew3wxZuPyX7E8
Files whose photo is in the trash stay unchecked until Restore them or Import as
new is chosen; the toast and undo label say what was restored.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The crop overlay now reports pointer positions to crop.drag instead of
computing rectangles itself. The Crop panel gets a visible Lock toggle, a
custom W × H entry, a dropdown chevron and a button label that names the
active ratio ("Original", a preset, or 1.37 : 1) instead of pixel sizes.
LR-CROP-RECT / LR-CROP-ASPECT parity rows updated.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Release: FreeBSD x86_64, Flatpak x86_64/aarch64, AppImage zsync updates; README Downloads
- linux: appimagetool -u embeds gh-releases-zsync update information; the .AppImage.zsync is
published beside each AppImage (zsync installed; checked in the job).
- flatpak: new job per arch repackages the Linux tarball into a single-file .flatpak
(packaging/linux/flatpak-bundle.sh + ai.storyteller.lightcraft.bundle.yml); the Flathub
manifest gets the CLI and the same commented finish-args; packaging-lint checks they agree.
- freebsd: new job builds lightcraft-<ver>-freebsd-x86_64.tar.gz in a FreeBSD 14.3 VM with the
freebsd.yml packages (packaging/freebsd/package.sh; layout dry run in packaging-lint).
- version, linux and web no longer use the release environment (they sign nothing), so a
branch dispatch dry-runs every unsigned job; the release job still needs macOS and Windows.
- README: Downloads section listing every build; docs/releasing.md updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Linux packages: describe LightCraft, not PhotoCraft, in the deb/rpm description
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <shared2@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A name that does not fit is cut with an ellipsis before the photo count (the
full name on hover) instead of running past it; scrolling the sidebar sideways
or widening it shows more. A row asks for room for at most 140 pt of its name,
so depth, not one long name, makes the sidebar scroll. Selection and hover
bars end at the panel's edge when the content is wider.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- ui: counts stay at the visible edge when the sidebar scrolls sideways
(with the row's colour behind them where a long name runs under); the
smart-album dialog's live count includes the album's folder
- catalog: a folder path is used as given (a name with a space at its edge is
not its neighbour: Remove acted on the wrong folder); a plain POSIX path
is checked against the folder without building a key (about half the cost
on a big library, with a test that it agrees with the careful check)
- engine: editing a smart album's rules keeps the folder the editor cannot
show; a shown folder that loses its last photo gives way to All Photos;
library.state and view.json name the folder only while it is shown; a blank
folder filter is no filter
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Every level indents one step more than its parent however deep it goes (the
five-level indent cap is gone). The whole sidebar scrolls in both directions:
rows report how wide they need to be, the widest one of the last frame sets
the content width (at least the panel's), and a bar appears only when it
overflows. Folder names are no longer cut in the tree; Create Album stays at
the visible edge.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- engine: a smart album made from a folder view keeps the folder; only a
folder that shows exactly its own photos can be the source (not the
startup disk, not a folder above disks, not one with no photos); a
folder source and a folder filter never coexist; a folder source with no
folder shows nothing; a saved view whose folder is gone opens on All
Photos; removing a folder above a disk needs `disk: true` even with one
disk mounted; library.folders documents volume/selectable
- ui: deep chains stop indenting after five levels, the grid title gives
way to the photo count (full name on hover), disclosure triangles have
spoken names, the remove dialog says what is included and has a disk
title; test temp folders are cleaned up on failure
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- catalog: a row is exactly one folder (no folding of single-child levels);
the UI starts a folder that holds nothing and leads to one folder open
- engine: a library folder is a source (LibrarySource::LibraryFolder with
Session::library_folder, like Local's browse): library.source
{kind: libraryFolder, path}, kept with the library's view, shown by
library.state, followed through folder.rename / folder.move and undo,
dropped back to All Photos when its folder is removed
- ui: clicking a folder replaces whatever the grid showed (Picks, Recently
Deleted, a Local folder…) instead of narrowing it with a filter chip; the
grid is titled after the folder
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Another way to explore the library next to By Date and Keywords: a tree of
the folders that hold imported photos, with counts (subfolders included),
under the disk or network share each is on. Local stays the way to browse
any folder on disk.
- catalog: Catalog::folder_tree and Filter::library_folder. Each disk is a
volume row (drive letter, Windows share, /Volumes/<share>, /media, /mnt,
gvfs, /net…), found from the stored paths alone, so an offline share costs
nothing. Levels that hold nothing and lead to one folder fold into one row;
depth is bounded. The tree and the filter read paths through one splitter
(separators, `.`, `..`, verbatim UNC prefix, repeated separators), and a
row that would cover other disks' photos is not choosable
- engine: library.folders, library.removeFolder (to Recently Deleted, one
undo step, no file touched; a whole disk or a folder spanning several
needs `disk: true`, the startup disk never), libraryFolder in
library.filter, chip; a chosen folder follows folder.rename / folder.move
and their undo
- ui: collapsible Folders section; the chosen folder's row is always opened
into view; right-click (row or triangle) Rename, Move, Show in Finder,
Remove from Library…; disk rows Remove Disk from Library…; both ask first
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Bilevel scans, CCITT fax images and 2- or 4-bit gray TIFFs were rejected with
"sample buffer too short". The `tiff` crate returns such samples packed, most
significant bits first, eight, four or two to a byte with each row padded to a
whole byte; the converter expects one byte per sample. They are now spread out
one per byte and scaled to 0–255 (255, 85 and 17 times the sample: exact) before
conversion. 3-, 5-, 6- and 7-bit samples, and planar images with more than one
sample narrower than a byte, are refused with an "unsupported" error.
The new test builds 1-bit (both photometric conventions, a width that leaves row
padding, set padding bits), 2-bit and 4-bit strips in code, plus a truncated
strip that must fail cleanly; it fails without the fix with the old error. A
600×400 bilevel image, uncompressed and CCITT G4, now decodes to the same pixels
PIL reads from it.
The module documentation no longer claims palette support: the `tiff` crate
refuses RGBPalette images on every read path, so the palette branch here has
never been reached. The parity row lists palette TIFF as missing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep macOS plain-key ratings, labels and flags with egui, adapting the
native menu ownership correction from PR #261 (4e012a5, Gardy).
Add Shift+6-9 and contextual grid Shift+P, recognize shifted digit
punctuation, and document the verified Lightroom Classic mapping.
Cover batch changes and undo, exactly-once advance, Compare targets,
text and crop context, physical number keys and native menu ownership.
Verified native demo/control screenshots and all seven cargo xtask ci gates.
Co-authored-by: Gardy <armandpro50@gmail.com>
The filmstrip painted selection chrome only for the active photo, so after
Select All or a ⌘/Shift-click multi-selection the other selected photos
looked unselected, although the selection itself was right.
Paint it as the grid does: every selected photo gets the selected cell
fill; the active one keeps its white frame and the others get the grid's
grey one.
Test: tests_scroll::filmstrip_shows_every_selected_photo samples a cell's
background in the headless render after Select All, after selecting one
photo, and after a toggle (what a ⌘-click runs); before the fix a selected
photo that was not the active one was painted like an unselected one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`--help`, the module doc and the README promise that a `--memory` session
"writes nothing", and the desktop app broke that in two places:
- `PrefsWriter` saved `ui.json` whenever the UI state changed (issue #164),
so a throwaway session's `thumbSize`, window layout or language replaced
the user's saved settings — the README showcase script does exactly that.
- The GPU crash sentinel from #136 armed itself in every session except
`LIGHTCRAFT_NO_PREFS` (issue #169); `with_init_marker` creates the
settings folder to hold `gpu-init.marker`, removes the marker and leaves
the folder, so a `--memory` run on a fresh home left an empty
`Library/Application Support/LightCraft` behind.
Now `PrefsWriter` carries its target path (`None` for `--memory`,
`LIGHTCRAFT_NO_PREFS` or no home folder) and writes nothing without one; it
also writes nothing while the session is the one offered when the library
can't be opened (the question window and Continue Without Saving, which
`docs/control-protocol.md` already describes as writing nothing) and
resumes when `app.openLibrary` ends it. `gpu_crash_check(in_memory)` neither
arms the sentinel nor removes a marker in a `--memory` session; it still
reads one (new `read_init_marker`), so GPU rendering starts off there too
and the next ordinary launch reports and clears it. A `--memory` session
still reads `ui.json`, but a damaged one is reported and left in place
rather than set aside. Normal launches are unchanged.
Tests: `prefs_writer_writes_nothing_in_a_memory_or_temporary_session`,
`memory_session_leaves_the_gpu_marker_and_folder_alone`,
`memory_session_leaves_a_damaged_ui_json_alone` (apps/lightcraft), and
`read_init_marker` coverage in the gpu crate's marker test.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`ExportOptions::from_json` reads what it recognises and defaults the rest, so
`longEdgee=400` exported at 3000 px, `longEdge=banana` at full size, and a
`watermark` of `5` or `{"text":"x","bogus":1}` passed with `ok:true`, while
`size: 3` was silently clamped to 0.5 of the short edge (#181, #183).
`from_json` stays lenient for stored prefs and dialog prefill. New
`ExportOptions::validate(cmd, params)` / `from_params(params)` check every key
against the option and target lists (unknown ones name the nearest known key),
every value's kind and range, and a `watermark` object's keys, kinds and ranges
(`size` 0.005..0.5 and `inset` 0..0.4 of the short edge, `imageWidth` 0.01..1 of
the width, `opacity` 0..1, `color` [r,g,b]), returning
`invalid parameters for \`app.export\`: …` like the other commands. The three
places that run an export use it — the desktop `app.export`, the headless
backend behind `lightcraft-cli run` and the MCP `export` tool, and
`lightcraft-cli render --opt` — and `export.savePreset` validates a preset's
params when it is saved. Built-in presets, the Export dialog's `to_json`,
Export with Previous and `render --opt` all pass unchanged.
The `app.export` param string (`lightcraft-cli commands`, `engine.commands`),
the MCP `export` tool schema and docs/parity.md LR-EXP-WATERMARK now list the
watermark keys, their units and ranges.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The desktop app's logger (StderrLog in main.rs) wrote to standard error
only. A launch from a desktop menu or the Dock has no terminal, so its
warnings were lost, and the panic log in the temp folder is gone after a
reboot: there was nothing to attach to a bug report.
apps/lightcraft/src/logging.rs replaces it (no new dependency):
- standard error plus logs/lightcraft.log in the settings folder, next
to ui.json (Linux $XDG_CONFIG_HOME/lightcraft/logs, never the library);
each launch rotates the previous log to lightcraft.1.log and .2.log, so
the log of a run that crashed survives the next start; the file is
capped at 16 MiB with one note
- records logged before the settings folder is known are buffered
(bounded) and written once the file is attached
- default levels: info for lightcraft*, warn for everything else;
LIGHTCRAFT_LOG=info|debug keeps its old meaning (a test compares it
with the old logger) and wins over RUST_LOG, which otherwise replaces
the default with env_logger-style directives
- never panics: an unusable folder or a failed write leaves standard
error as the only sink; a poisoned lock is taken back
- runs with LIGHTCRAFT_NO_PREFS (tests, scripts) log to standard error
only, so they don't rotate away the user's own logs
- a panic hook chained after the engine guard's adds escaped panics to
the file (try_lock, so a panic while logging can't hang the app);
lightcraft-panics.log in the temp folder is kept
The logger is installed first in main and the file is opened after
argument parsing (--version, --help and usage errors leave no file).
The start-up and control-server messages in main.rs and
control_server.rs go through it instead of eprintln!, and a --control
or LIGHTCRAFT_CONTROL_PORT value that isn't a port is now reported.
Documented in README (Quick start, Logs), --help and docs/whats-new.md.
`fn snapshot` wrote every reply but never looked at `ok`, so a script with a
bad `ui.set`, an unknown command or a failed `ui.screenshot` exited 0 while
the PNG showed the wrong view or was never written. The run now counts
replies with `"ok": false`, keeps going (every reply still reaches stdout and
the final `-o` screenshot is still written — continue-and-report, like
`run --keep-going`) and returns `N scripted request(s) failed` afterwards,
which `main` turns into exit 1. Usage text and docs/control-protocol.md
"Headless rendering" state the contract.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Split out of #240 without the compressed decoder (crates/raw/src/vendor/orfc.rs
is not part of this change; compressed ORF stays preview-only).
- The layout is read from Exif CFAPattern (0xa302, repeat counts in either
byte order) when it describes a 2x2 Bayer cell. BGGR and GBRG were not
reachable before.
- Without a usable tag the green diagonal is still found from the samples,
now from totals of 32x32 blocks instead of single-pixel differences, so
fine texture no longer outweighs a small red/blue difference. The old
version indexed the samples directly; this one reads them with get().
- probe_info no longer unpacks 12-bit packed ORFs that state their layout
(XZ-2: under 0.1 ms; it ran the same unpack as a decode before, which
takes 11 ms). 16-bit files still read their samples to find the bit depth.
Tests: stated layouts in both byte orders, unusable tags falling back to the
samples, the texture case, hostile areas for the fallback, probe_info equal
to decode; a corpus test comparing the E-1, E-400 and XZ-2 mosaics with
their embedded JPEGs under every layout.
No user-visible change yet for the bodies whose layout the old guess got
wrong (E-M1 and E-620 are BGGR): they write compressed ORF.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`select_photos {"ids": [9999]}` answered `selected: 1` and left the session
with no usable active photo; `develop.get {"id": 9999}` answered too.
`library.select` now checks every id (and `active`) against the catalog
before touching the selection and returns `no such photo N`, so the
previous selection and active photo stay in place; `photo_arg` (develop.get,
photo.inspect, photo.allMetadata, history.list) does the same for an
explicit `id`. Engine and MCP tests cover every select mode.
Fixes#182
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
On macOS every shortcut that appears in the menu bar was left to the native
menu, and the egui shortcut handler skipped those keys. AppKit only offers a
key press to the menu bar when it carries ⌘ or ⌃, or is a function key; a
plain key (E, C, H, M, I, K, D, 0–9, P, U, Y…) or one with only ⇧ / ⌥ (⇧P,
⇧V, ⌥Y…) goes to the window's winit view instead. So all of those did
nothing on macOS, while keys outside the menus (Space, G, X, ⌫) and ⌘
shortcuts worked.
`publish_shortcuts` now lists only the shortcuts the menu bar really receives
(`menu_delivers`: ⌘, ⌃ or a function key) in `native_shortcuts`. The others
keep their accelerator on the menu item, so the menu still shows the key, and
egui runs them.
Checked in a release build by pressing keys (not via the control channel,
which bypasses the native menu): C opens Crop & Rotate, E Edit, ⇧P Presets,
3 / 0 set and clear the rating; ⌘⇧H toggles the histogram once per press;
typing "c" in the search field types and leaves Crop closed. The same build
without this change ignored C, ⇧P and 3. ⌃ shortcuts (merges) are unchanged
and were not tried.
Regression test: native_menu::tests::single_key_shortcuts_stay_with_egui
(fails with the previous behaviour: "E must be left to egui").
* About: contributors compiled in from contributors.json (Contributors + Models tabs, grab bag + table, sortable, name toggle)
Adds the craftrules standard (standards/contributors.md) to LightCraft: contributors/contributors.json
is generated by craftrules/scripts/contributors.py and baked into the binary by
crates/ui-egui/build.rs (merged next to the tr_format! generation) as static tables. The About
dialog gets tabs About · Contributors · Models. Each contributor line: @username, opt-in
display/real name, PRs, commits, lines +/−/Δ, binary assets +/−, first/last commit. Sortable by
every column; alphabetical ignores case and the leading @. Tabs and the list view are
control-channel widgets (button:aboutTab-*, button:creditsTable / button:creditsGrabBag).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Contributors docs: craftrules is two levels up (../../craftrules)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Contributors: self-submitted names in contributors/people.toml; AGENTS.md credit-yourself section
build.rs reads contributors/people.toml next to the generated contributors.json and shows a name only
for entries the generator verified as written by that person (craftrules standards/contributors.md).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Contributors: regenerate (verified people.toml entries)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Contributors: regenerate after rebase onto main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Contributors: regenerate (verified people.toml entries)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>