get_api shared the 4 MiB limit for small answers. A launcher version list of
100 releases can pass that: SolidCraft's was 6.5 MB (craft-launcher#1021).
Other small requests keep 4 MiB.
A Flatpak launcher wrote versions.json inside its own sandbox, and no Flatpak
app could read the launcher's folder, so pins never reached Flatpak apps, nor
host apps started from their menu entry. The file now sits in its own folder,
ArtCraft/versions under the host's per-user data folder, found from inside a
Flatpak too (HOST_XDG_DATA_HOME, else ~/.local/share). A Flatpak app reads it
with one read-only grant for that folder alone (pins::FLATPAK_GRANT).
folders::host_data_local_dir replaces install's private copy of the same rule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
finish_check now re-reads the choice before looking at the answer, so an app
the launcher pinned mid-check reports UpToDate and an up-to-date managed()
whether the network found a release, nothing, or an error.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
finish_check used the choice cached when the check started, so a launcher
pin made during the network request still opened the stable prompt. It now
re-reads the choice when the check finishes, as offer does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review fixes for #449:
- A prompt opened while the app followed stable stayed open after the
launcher pinned it, and its Update button still installed the stable
release. Reading a managed choice now closes a prompt that is not
installing, and an install re-reads the choice before it starts. A
running or finished install keeps its prompt, which tick reports through.
- Pins::tracks listed an explicit Stable entry despite its docs.
- The pretend-version child test read the developer's real pins file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The launcher lets someone keep an app on an older release or on nightlies,
but the app's own update check never knew, so a pinned app prompted to
update on every start and "Update and restart" undid the downgrade.
`pins` is one small file the launcher writes (versions.json in its per-user
data folder, or the absolute path in CRAFT_VERSIONS_FILE) and apps read:
app id to Stable, Nightly or Pinned(tag), the launcher's own serde shape.
A missing or damaged file means everyone follows stable; an entry a newer
launcher wrote is left to the launcher and kept on save.
`Updater` reads it before each check and before an offer. An app kept off
stable gets no prompt and no network call; a requested check reports
UpToDate, and `Updater::managed()` says why, so the public Status enum is
unchanged and no app breaks on a rev bump.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The update prompt drew the app name's first letter in a blue square and used that blue for
its buttons and highlights. App gains accent and developer, and the prompt now draws:
- App::icon_png (decoded once per context, PNG only, at most 1024 px a side and never past
the renderer's texture limit), keeping the letter badge for an app without a usable icon;
- App::accent filling its buttons, the badge and an UPDATE chip, with white or black text,
whichever contrasts more, so every Craft app's accent reaches WCAG AA (4.5:1) for these
small labels; the What's new link is body text with an accent underline (accent-coloured
text missed 4.5:1 for several apps). No accent keeps the default blue;
- "by <developer>" beside the app name: App::developer, ArtCraft (app::DEVELOPER) for every
app in the storytold organisation unless the app passes its own;
- its own button states: backgrounds for unfilled buttons (LATER, DISMISS) when hovered and
pressed, as the host app's style can hover in a colour its dark text doesn't read on, and a
2 px focus ring in the body text colour on every button (egui shows focus in that state)
and on the What's new link.
Apps opt in by passing their own App, e.g.
App { icon_png: Some(include_bytes!("…128x128…png")), accent: Some([0x3f, 0x5f, 0xe0]), ..suite::WORDCRAFT }.
* craft-print: log why the printer list is empty when lpstat can't run; document Flatpak needs
Flatpak apps saw "No printers found" with working CUPS printers: the Freedesktop
runtime has libcups but not lpstat/lp, and the sandbox had neither the CUPS socket
nor Avahi (driverless printers come from lpstat -e). Log the spawn failure and say
in the README what a Flatpak must bundle and grant.
* craft-print: log a failed lpstat -p -d; README separates what each Flatpak piece is for
Review (CodeRabbit): a non-zero lpstat exit was dropped silently, and the README
said any missing piece empties the printer list when only lpstat and the socket do.
* craft-print README: driver options need the PPD, remote servers need network, socket paths are remapped
Review (CodeRabbit): printer_options reads /etc/cups/ppd before lpoptions, so it is
the PPD, not lpoptions, that a Flatpak lacks; a client.conf server is reached only
with --share=network.
* craft-print README: scope the CUPS_SERVER override to one app; describe the stricter socket wrapper
Review (CodeRabbit): flatpak override without an app id applies to every app.
* craft-update: a Windows MSI copy updates itself in its own context, wherever it is installed
Self-update only recognised an MSI copy by the launcher's per-user folder (the HKCU InstallDir
key, or %LOCALAPPDATA%\Programs\<App>). Every app installed for everyone, in
Program Files\ArtCraft\<App> or the older Program Files\<App>, was treated as a loose folder and
updated by unpacking the portable zip over Program Files without permission.
- msi_context moves here from the launcher (craft-launcher crates/native/src/msi_context.rs at
b827fed, unchanged apart from visibility, MsiScope and scope_arguments). Windows Installer's
own registration decides a copy's context, for the launcher and the apps alike, so the
launcher can drop its copy and both routes follow one rule.
- New msi_copy module: a copy is an MSI copy when an App Paths entry names its exe (a registry
read, cheap enough for status checks). Its update asks Windows Installer for the context; the
App Paths hive is only the fallback when that lookup cannot run.
- The update stops, before the app exits, when the copy is registered in both hives, the lookup
is unclear, or Windows lists the app more than once.
- After the update the helper starts the exe the registrations name now, since the package may
have moved an exact old default folder.
- A failed self-update now names the Windows Installer log, as the launcher's installs do.
- The launcher's own per-user package is recognised and updated exactly as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* craft-update: a copy only takes the Windows Installer context of its own hive
Review follow-up (CodeRabbit on #419). The product code for the context lookup came from the
app's only uninstall record, which was not proven to own the running exe. A package writes App
Paths under HKMU, always the hive of its own context, so a registration whose Windows Installer
context differs from the hive that named this exe belongs to another copy. The update now stops
then, instead of taking the other copy's context.
Also documents the remaining helpers the diff touches.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* craft-update: an app in the old per-user folder is judged by its registration first
Review follow-up (CodeRabbit on #419). The folder rule for the launcher's per-user package also
matched an app installed in %LOCALAPPDATA%\Programs\<App>, and it ran before the App Paths
check, so such a copy skipped the product-count, conflict and context checks. Now an app is
judged by its registration first; the folder rule only covers an app copy no registration names.
The launcher in its own folder still keeps its old path whatever is registered: on a real PC it
has an App Paths entry and a stale second uninstall record, and registration-first would have
stopped its self-update.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* craft-update: when the Windows Installer lookup cannot run, the uninstall records decide
Review follow-up (CodeRabbit on #419). The fallback trusted the App Paths hive, so a stale entry
or a product registered in both scopes could run the update in the wrong context. The fallback
now follows the launcher's rule: the context every uninstall record of the product agrees on
(msi_context::single_scope over HKLM in both views and HKCU). Missing or mixed records stop the
update, and an agreed context that differs from the App Paths hive still stops it as another
copy's registration.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-up (CodeRabbit on #423). If LOCALAPPDATA\Programs or Programs\ArtCraft is itself a
junction, the exe's folder resolves through it but a default path built from a resolved
LOCALAPPDATA does not, so a default install was planned as a chosen folder. The Windows layer now
resolves each complete default folder (new default_folders) and hands them to the planner, which
only compares.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-up (CodeRabbit and CI on #423):
- The profile roots and LOCALAPPDATA are canonicalised like the exe's folder, so a profile behind
a junction or a subst drive still matches its default folder and still counts as the profile.
- When a failed move leaves no program to start, the helper starts nothing, so the recorded
"run its installer" message is not replaced by a restart error.
- The default-folder test builds its expected path the way the plan does, so it also passes
where Path::join uses '/' (Linux CI).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reported by FredApps: PhotoCraft built from main as 0.17.0-integration.20261029.f5b3f123 offered
"Update and Restart" to 0.17.0, a downgrade. Semver puts a pre-release before its release, and
the update check and the self-update guard compared with plain semver.
New version::offers_update: a pre-release label the suite publishes (alpha, beta, rc, nightly)
sorts as before; any other label marks a build from source, which is only offered a strictly
higher X.Y.Z. check_for_update (through a new testable offered_release) and the self-update
guard both use it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Temporary (craft-libs#422, step 4): new msi_migrate module, to be removed two releases after the
app packages become per-machine only.
The released packages refuse a per-machine install while a per-user copy exists, so the move
removes first and installs second: remove the per-user product (no permission prompt; if that
fails nothing has changed), install the new version for everyone, and if that fails or
permission is refused, install the new version for the account again in its old folder.
Settings and documents live outside the program folder and are never touched.
Which copies move: a per-user default folder goes to Program Files\ArtCraft\<App>; a chosen
folder outside the profile moves in place; a chosen folder inside the profile keeps updating
for the account. The launcher's own package never moves.
Tested with real Windows Installer runs of a renamed dual-scope package carrying the apps' own
install-policy.wxi: the move with the permission prompt accepted (twice), and with it refused
(the app came back for the account at the new version, the failed log kept). Also unit tests for
every plan and the order of steps, and a mutation check (9 of 9 killed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-up (CodeRabbit on #419). The fallback trusted the App Paths hive, so a stale entry
or a product registered in both scopes could run the update in the wrong context. The fallback
now follows the launcher's rule: the context every uninstall record of the product agrees on
(msi_context::single_scope over HKLM in both views and HKCU). Missing or mixed records stop the
update, and an agreed context that differs from the App Paths hive still stops it as another
copy's registration.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-up (CodeRabbit on #419). The folder rule for the launcher's per-user package also
matched an app installed in %LOCALAPPDATA%\Programs\<App>, and it ran before the App Paths
check, so such a copy skipped the product-count, conflict and context checks. Now an app is
judged by its registration first; the folder rule only covers an app copy no registration names.
The launcher in its own folder still keeps its old path whatever is registered: on a real PC it
has an App Paths entry and a stale second uninstall record, and registration-first would have
stopped its self-update.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-up (CodeRabbit on #419). The product code for the context lookup came from the
app's only uninstall record, which was not proven to own the running exe. A package writes App
Paths under HKMU, always the hive of its own context, so a registration whose Windows Installer
context differs from the hive that named this exe belongs to another copy. The update now stops
then, instead of taking the other copy's context.
Also documents the remaining helpers the diff touches.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Self-update only recognised an MSI copy by the launcher's per-user folder (the HKCU InstallDir
key, or %LOCALAPPDATA%\Programs\<App>). Every app installed for everyone, in
Program Files\ArtCraft\<App> or the older Program Files\<App>, was treated as a loose folder and
updated by unpacking the portable zip over Program Files without permission.
- msi_context moves here from the launcher (craft-launcher crates/native/src/msi_context.rs at
b827fed, unchanged apart from visibility, MsiScope and scope_arguments). Windows Installer's
own registration decides a copy's context, for the launcher and the apps alike, so the
launcher can drop its copy and both routes follow one rule.
- New msi_copy module: a copy is an MSI copy when an App Paths entry names its exe (a registry
read, cheap enough for status checks). Its update asks Windows Installer for the context; the
App Paths hive is only the fallback when that lookup cannot run.
- The update stops, before the app exits, when the copy is registered in both hives, the lookup
is unclear, or Windows lists the app more than once.
- After the update the helper starts the exe the registrations name now, since the package may
have moved an exact old default folder.
- A failed self-update now names the Windows Installer log, as the launcher's installs do.
- The launcher's own per-user package is recognised and updated exactly as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 02 §4: the decoder refuses the G5 with Error::Unsupported; the embedded-JPEG fallback is the
caller's, not the decoder's.
- 04: the Auto-slot claim holds outside the white-balance preset captures.
- The record README no longer says it holds everything in the order it happened: the derivation
trail stays at the pinned LightCraft revision.
- docs/clean-room.md allows that split for a derivation done in another repository, so the
standard and the CRW record agree.
main's workspace lints now deny cognitive_complexity above 30, and decode_entropy scored 41.
The emit macro becomes a Chains struct, unstuffing its own function, and the three stream-end
checks one. Same algorithm: the pinned CC0 digests still match byte for byte.
Also correct two record lines: the G5 is the one file that does not match, and the frozen
code tables are not the only logs that ship.
* Add craft-dialogs: native file and message dialogs that never block the event loop
Requests queue and show at the end of the frame on the main thread; answers come
back with the app's continuation on a later frame. One dialog at a time, a drop
guard and a panic guard so a dialog never stays open, and an override for tests.
On macOS, Open and Folder show as a free-standing NSOpenPanel
(beginWithCompletionHandler, which rfd can't show), Save and message boxes as
sheets through rfd's async dialogs, never falling back to runModal. Filter
extensions match in any letter case on Linux.
* craft-dialogs: Dialogs::raise brings the open macOS panel to the front
* CI: dialogs workflow (Linux clippy and tests for craft-dialogs)
* craft-dialogs: per-instance macOS panel, parent window check, override panic guard, CI lints macOS code
* craft-dialogs: raise only a visible panel; Open and Folder need a running app
A system install was offered only as a download. Reinstalling that bundle
through Flatpak's install-bundle action is denied without a password dialog
when polkit cannot fill in $(path). Use pkexec on the host instead, and keep
per-user bundles on flatpak install --user.
Co-authored-by: Cursor <cursoragent@cursor.com>
* ci: window and ui run macOS only when started by hand
macOS runners are kept for releases. Pushes and pull requests run Linux and Windows; workflow_dispatch runs all three.
* ci: the other workflows run macOS only when started by hand
atomic, docpath, ai-core, i18n, print, pdf, update and keymap follow window and ui: Linux and Windows on pushes and pull requests (keymap: Windows), all their platforms on workflow_dispatch.
* craft-ui: combo boxes whose menu grows back with its contents
egui's ComboBox lays its menu out in the height it had on the previous pass,
and its scroll area takes no more than that. A filtered list that shrinks (a
search with no matches) never grows back when the search is cleared
(emilk/egui#5225, #5138). craft_ui::combo::menu offers its contents the full
height on every pass; craft_ui::combo::combo draws the combo box button
around it. The Font Manager's Language, Script and Sample menus use it.
* craft-ui: combo and menu take the close behaviour
A menu with a search field that closes itself on a pick (DesignCraft's font
menu) closes on a click outside only.
* craft-ui: ComboBox, a drop-in for egui's combo box, with header menus
craft_ui::ComboBox has egui 0.37's ComboBox builder and button (same
atoms, style classes and icon, pixel-identical in a test) with a menu that
grows back. show_ui_with_header and show_rows_with_header keep a search
field in place and scroll only the list; combo::Menu is the same menu under
a field an app draws itself. clippy.toml disallows egui::ComboBox.
The Font Manager's Language menu, the keymap dialog's combo boxes and the
print dialog's printer and duplex choices use it.
* craft-ui: combo menus keep to the room on their side, and footers
A menu is never taller than the room on the side of its field it opens on,
so an unbounded height fits on screen and scrolls. The side is settled once
the menu is measured and kept while it is open, so a menu that shrinks or
grows while typing doesn't jump to the other side of its field.
Footer forms keep a row under the scrolling list. Menu::new takes the
field's Ui for its default height and frame, and Menu::open shows the menu
under a text field that keeps the focus while typing.
* craft-ui: combo menus open by the app keep their side, and take picks
Menu::open takes &mut bool: the menu clears it when it closes (close
behavior, Escape, ui.close(), the field losing the focus to anything but
the menu), so a click on a row under a focused text field is picked.
The side settles only once the menu was shown and measured, also for
menus the app opens (Popup::open_id). A footer that changes height
remeasures the menu before it is shown. The popup frame's margins come
from the popup style; only above/below alignments are tried.
* craft-ui: a resting pointer over a combo menu doesn't keep it open
The menu counts the pointer as having moved into it only on a press or
click. A footer height change also requests a repaint, in case the
discard is denied.
* craft-ui: a footer menu opens without a second sizing pass
A footer height change no longer discards the pass or forces another
sizing pass: the menu measures again on the next pass and settles its
side then. A forced sizing pass ran a search field's focus request in a
pass where the field can't take the focus, so a footer menu opened with
the search unfocused.
* craft-ui: tests use Ui::scope_id
* craft-ui: hide the measuring pass of a footer menu, cap remeasures
The pass that measures a menu before its side is settled is laid out but
drawn fully transparent, so no frame shows a side or height the menu then
leaves. A footer that changes height on every pass remeasures twice per
open, then settles.
* craft-ui: tests satisfy clippy
A no-plane PowerShot frame whose payload is shorter than plane + header ends
inside the scan window with the record's real `ff d9 00` trailer, so rule (a)
read the `ff d9` as a plane byte, reported 12 bits and then refused a valid
file. Both discriminator signs now require a payload long enough to hold a
plane, so a flat G1 frame decodes instead of being rejected.
Also from the review: clamp samples to the declared depth so a damaged stream
cannot put more than `bits` into a consumer's `1 << bits` LUT, report zero
sensor dimensions as corrupt rather than as a limit, take a signed ImageInfo
rotation modulo 360 (so -90 is 270), warn against pairing BlackLevel::values
with Header::cfa by index, and pin actions/cache in the crate's workflow.
Tests cover the trailer payload, end-of-block chain values, the clamp, zero
dimensions, both white-balance slot layouts with every Canon setting, and each
rotation spelling. The 11 CC0 samples still decode exact with
CRAFT_CRW_REQUIRE_SAMPLES=1.
The crate's record should hold what the decoder is built from, not the process
that produced it. The starting assumptions, the plan, the container notes, the
entropy-derivation log, the chart calibration, the probe scripts and their
observational logs are not part of it. The frozen-table JSONs and
tools/gen_tables.py stay, so src/tables.rs can still be regenerated and audited.
Renumber the surviving documents 01-05 in reading order and update every
reference to them, so the record reads in order and cites nothing that is no
longer there.
AGENTS.md and CLAUDE.md go back to main: repository instructions are not part of
this change.
The branch gained review commits after the port (chrono for a date conversion that this
crate does not contain, a test skip macro); the decoder and the clean-room record are the
same at both revisions, so the notes name the tip and the ported revision.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The clean-room Canon CRW decoder written in LightCraft (crates/raw/src/vendor/crw.rs,
crw_tables.rs, branch clean-room-crw-format-decode, revision 70c755e4) becomes the shared
crate craft-crw: the CIFF heap container, the entropy coding derived by black-box analysis
(frozen code tables for selectors 0-2), sensor black and white levels, as-shot white
balance, orientation and the make/model/time records. Public types are the crate's own
(Error, Limits, Cfa, Rect, BlackLevel, Header, Image); the capture time is handed on as
Unix seconds so no calendar code lives here; LightCraft's shared plateau estimate is
copied as saturation_plateau. Algorithm, tables and thresholds are unchanged.
Tests: the ported synthetic round-trip, table-structure and hostile-input tests; a
proptest mutation sweep (tests/robust.rs); and tests/samples.rs, which pins the eleven
CC0 raw.pixls.us sample files (tests/samples.json: URL, SHA-256, licence) to the SHA-256
of the verified decode plus levels, white balance and metadata, so the port is proven
byte-identical to the LightCraft decode without redistributing the reference TIFFs.
scripts/fetch-crw-samples.py downloads the files outside the checkout; CRAFT_CRW_SAMPLES
and CRAFT_CRW_ORACLE point the tests at them. The crate builds for wasm32.
The whole derivation record (starting assumptions, plan with the maintainer's decisions,
source log, container and payload notes, derivation log, frozen format, verification,
colour/levels notes, private-corpus and chart notes, scripts, logs and the frozen table
JSON) moves verbatim to crates/crw/clean-room/ with a README that indexes it and maps the
LightCraft paths it cites; tools/gen_tables.py regenerates src/tables.rs from the JSON,
byte for byte.
Repository: .github/workflows/crw.yml (fmt, clippy, tests on Linux, Windows and macOS;
wasm check; the sample-file job with cached downloads), README and ATTRIBUTION rows,
NOTICE source provenance, docs/clean-room.md (the clean-room standard a format crate
follows here), and AGENTS.md expanded with the start-of-session steps, the new-crate
checklist, clean-room, assets and test rules; CLAUDE.md points at it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>