Group related dialog and inspector settings in reusable frames with optional accessible headings.
Makes the reusable part of [WordCraft #4038](https://github.com/storytold/wordcraft/pull/4038) available to other Craft apps. Includes API documentation, integration guidance and regression coverage. Consumer adoption remains separate; applications retain their themes, localization and command adapters.
Validation on Windows: workspace formatting, strict Clippy and tests; craft-ui Clippy/tests with all features; craft-ui WASM check with all features. Upstream CI results are tracked on this PR.
Includes the shared test-portability prerequisite [#267](https://github.com/storytold/craft-libs/pull/267); the feature itself is independent of the other UI proposals.
[Integration guide](https://github.com/fluorine/craft-libs/blob/fad5c05170d43f9bdf86c6063131e28669fcb176/crates/ui/README.md).
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
<!-- coderabbit-ai-generated:v1 -->
## Summary by CodeRabbit
* **New Features**
* Added customizable sections for grouping content vertically, with optional headings and configurable spacing.
* Sections can fill the available width or use a caller-defined width, and return the result of their content callback.
* **Documentation**
* Added usage guidance and an example for styling sections in dialogs and inspector panels.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
* update: an unsigned Windows copy gets Download instead of Update and Restart
The update helper installs nothing for an unsigned copy, so the prompt's
Update and Restart button on a local or unsigned build downloaded the
release and then stopped with "this copy isn't signed". is_self_updating
now answers false for an unsigned copy on Windows, and the prompt offers
Download. CRAFT_UPDATE_DEV still offers the update.
* update: check that a Windows copy can install before downloading
windows_precheck ran after the download, so a copy that could not install
downloaded the whole release first. The checks that do not depend on the
download now run before it; a new MSI's signature is checked after it.
The cache folder is created first, so a first update passes the check.
* update: a failure recorded before the running program is not reported
take_outcome reported any recorded failure at the next start, so a copy
put in place after an update had failed still opened with "The update
could not be installed". A failure record older than the running program
is now removed without being reported. A failure recorded after the
program was put in place, such as a failed helper's, is still reported.
* update: a Cargo build with a separate build directory is a development build
With build.build-dir set, Cargo keeps .rustc_info.json and the profile's
.fingerprint, deps and build folders in the build directory, and the
program's own folder has only its lock file. Such a build counted as an
installed copy and was offered Update and Restart. A profile folder with
Cargo's .cargo-lock under a target root with CACHEDIR.TAG is now a
development build, and gets the download link.
* update: a cache folder that can't be created is reported as that
same_cache_as_helper ignored the error from creating the folder, so a
failure read as a cache in the wrong place. It now names the folder and
the error.
* craft-update: a slow macOS signature check is not reported as a bad signature
`signed_by` gave `codesign` 120 seconds and treated every failure as a bad
signature, so a Mac that could not finish in time was told the app "isn't
signed by its developer".
The check now has 10 minutes and checks one bundle at a time. A job waiting
for its turn reports `JobPhase::Waiting` and can be canceled. A timeout, a
`codesign` that cannot start or is stopped by a signal, and any exit other
than 1 or 3 report that the signature couldn't be checked. Exit 3 refuses the
app as not signed by its developer. Exit 1 refuses it with `codesign`'s own
reason, because `codesign` also exits 1 for a bundle it cannot read. The app
is not installed in any of these cases.
On macOS, an install or uninstall now stops when `pgrep` gives no answer,
where a timeout used to read as "not running". `pgrep` has 30 seconds. An
install looks again after the signature check, which can wait behind other
apps' checks, and does not replace an app opened during the wait. A
self-update skips that second look.
* craft-update: a copy that cannot be replaced gets Download
A macOS bundle, an AppImage or a Linux standalone program was offered
Update and Restart even when the update could not replace it, so the
update failed only after the whole download.
`current_at` now leaves out such a copy when `access(2)` refuses what
the update needs. On macOS that is adding a subdirectory to the parent,
and adding a subdirectory to and deleting the bundle. On Linux it is
writing and searching the folder. A macOS copy under App Translocation
is also left out. Any other error from the check leaves the update on
offer, and `CRAFT_UPDATE_DEV` still offers it.
* craft-update: a macOS standalone copy that cannot be replaced gets Download
macOS `detect` returns `Install::Standalone` for a loose program, and
the update replaces it by writing beside it, as on Linux. `can_replace`
now checks that folder on macOS too, so a copy in a folder the user
cannot write gets Download instead of failing after the download.
`CRAFT_UPDATE_DEV` still offers the update.
* craft-update: a standalone copy whose share files cannot be replaced gets Download
The update replaces a standalone copy's files under `share` as well as
the program when the app's `share/<id>` folder sits next to `bin`. The
offer now also checks those folders, the `applications` and
`icons/hicolor` folders a release writes to, and the nearest existing
parent of one that doesn't exist yet.
* craft-update: a standalone macOS program without a release tarball gets Download
On macOS a program outside an app bundle counts as a standalone copy, and
the self-update replaces it from a macOS .tar.gz. Releases carry a disk
image and a command-line zip for macOS and no .tar.gz, so the prompt showed
Update and Restart and the update stopped with "has no Tarball (.tar.gz)
for macOS".
The prompt now also asks release_installs, which on macOS offers Update and
Restart to a standalone program only when the release lists that asset.
Without it the prompt shows Download. A release whose file list was not
loaded keeps the offer, because the install fetches the full release first.
CRAFT_UPDATE_DEV still offers the update. Other platforms and other kinds of
install are not checked against the release.
* craft-update: CRAFT_UPDATE_DEV no longer skips the release check
A standalone macOS program run with CRAFT_UPDATE_DEV was still offered
Update and Restart for a release without a macOS .tar.gz, and that update
failed. release_installs now checks the release for that copy too.
release_installs_at takes the executable, the opt-in setting and the
platform, and a new test checks a loose program in a temporary folder
against releases with and without the .tar.gz. The tests use SoundCraft's
release files, since craft-launcher ships no command-line zip.
* craft-update: a failed self-update keeps its download for the next try
On macOS, Linux and FreeBSD, prepare_update removed the downloaded
release file whether or not the install succeeded, so every retry after
a failed copy, signature check, package install or swap downloaded the
whole release again. A failed install now keeps the file, as install_at
already does, and the next try reuses it through Http::download, which
checks it against the published SHA-256 first and fetches it again when
it does not match.
- The cache folder's self-update-download.txt records the name and size
of the release file before the download starts. A self-update for a
different release file removes the finished and partial download that
the record names, so each app keeps the files of one release at most.
- An install that succeeds removes the file and the record. So does a
failed install whose record could not be written, since nothing would
remove that file later.
- On macOS, when hdiutil cannot attach the disk image, the file is still
removed: an image left attached by an install that ended part way
makes hdiutil refuse the same file with "Resource busy", while a new
download of it attaches.
- Windows is unchanged: its update helper removes the download after
the app exits.
* craft-update: a macOS install that stopped partway no longer blocks the next install
When a macOS install ends before it finishes (the app quits or
crashes), the copy it was making stays beside the app and the update's
disk image stays attached. The next attach of the same file fails with
"Resource busy".
- Each staging folder's name now carries the installing process's id,
and the folder holds an empty marker file. Before it stages a copy, an
install removes the marked staging folders of processes that are no
longer running, and their empty unmarked ones. It also removes a
staging folder from earlier launcher versions when it holds only the
app's bundle and has not changed for a day. Backup folders and other
apps' folders are left alone.
- Before it attaches an image, an install detaches the images that
earlier installs of the same app left at their mount points in the
cache, when their process has ended or, in this process, when no
install uses them any more.
- When the attach fails, the install detaches its image if it is
attached with no volume mounted and no other attach of the same file
is running.
- A staging folder that can't be removed, or an image that stays
attached, is logged, and the error of a failed install says so after
its reason.
* craft-update: the kept-download test accepts either signature refusal
An unsigned bundle is refused as "isn't signed" today and as a failed signature check once codesign's exit status is read, so the test checks that the install stopped on its signature.
* craft-update: the share check covers nested and pixmaps folders
A standalone copy in a prefix gets Update and Restart only when the
update can write every `share` folder it replaces files in. The check
covered `share/<id>`, `share/applications` and `share/icons/hicolor`
only, but the update writes a copy of each file next to its
destination first. A folder inside `share/<id>` (such as `models`), an
icon folder such as `icons/hicolor/256x256/apps`, or `share/pixmaps`
(where an older flat release puts its icon) that the user can't write
stopped the update after the whole download.
The check now also takes in every folder inside `share/<id>`, each
`icons/hicolor/<size>/apps` folder that holds the app's icon, and
`share/pixmaps`. Icon folders of other packages and other themes are
not checked. A linked folder is checked but not walked. The walk reads
at most 4,096 folder entries, so a large tree does not slow every
start; a folder past that is reported by the install as before.
The same check runs for a standalone copy on Linux, so a Linux copy
whose nested share folder or `share/pixmaps` it can't write now gets
the download link too.
* craft-update: a release folder that can't be read stops a standalone update
A standalone update in a prefix copies every file under the release's
`share` folder. The walk skipped any folder it could not list, and the
archive keeps each folder's mode, so a folder that nobody may list left
its files out while the update reported success. An error reading an
entry of the release's `bin` folder was dropped the same way.
These are now errors that name the folder, and the update stops before
any installed file changes. A release without a `share` folder still
updates.
A release whose `share` or `bin` is a link was followed, so the update
copied files from outside the release into the prefix. A `share` or
`bin` that is a link, or a `share` that is a file, now stops the update.
Links further down under `share` are still skipped.
The unpacked release is removed after a failed update even when it holds
a folder that its owner can't list. Before, such a folder stayed in the
cache.
* craft-update: the share check covers doc, metainfo and mime, and follows links
The share check for a standalone copy in a prefix now also takes in
`share/doc/<id>` and every folder inside it, `share/metainfo` and
`share/mime/packages`. Release archives such as VectorCraft's Linux and
FreeBSD ones put files in all three.
The walk now follows links, as the update's writes do. A copy whose
`share/<id>` is itself a link had none of its folders checked, and a
folder behind a link inside `share/<id>` was not checked either. A
folder the walk reaches twice is read once.
When `doc/<id>`, `metainfo`, `mime/packages` or `pixmaps` is missing,
the check no longer falls back to `share` itself. A copy whose `share`
the user can't write, but whose app, `applications` and `icons/hicolor`
folders exist, gets Update and Restart again, as it did before the
previous commit.
The entry limit of 4,096 now applies to each of `share/<id>`,
`doc/<id>` and `icons/hicolor` separately, and it counts files and
folders. A large app folder no longer turns off the icon folder check.
In a folder with the sticky bit, such as `/tmp`, the user can rename
over a file only when the user owns the file or the folder. A copy now
gets the download link when such a folder holds one of the app's files
that another user owns: the program itself, its other programs
(`<id>-<name>`, such as `<id>-cli`), anything in `share/<id>` or
`share/doc/<id>`, or a `share` file named `<id>.<ext>` or
`<bundle id>.<ext>`.
An icon size that the installed copy doesn't have yet is still not
checked, and a read-only folder that the user added under `share/<id>`
does count. Linux runs the same checks for a standalone copy, and the
sticky check on the program file for an AppImage.
* craft-update: the app-program check has its own name
The check for the app's programs in a bin folder was named is_program, the name of the Windows check for an MZ header, so the crate did not build on Windows. It is now is_app_program.
* craft-update: a missing doc, metainfo or mime folder is checked through share
A Linux release writes files into `share/doc/<id>`, `share/metainfo`
and `share/mime/packages`, and the update creates each of these
folders when it is missing. When one was missing, the share check took
its nearest existing parent inside `share` and never `share` itself. A
copy whose `share` the user can't write and that lacked one of these
folders was offered Update and Restart, and the update failed after
the whole download.
For a missing `doc/<id>`, `metainfo` or `mime/packages`, the check now
takes the nearest existing parent with `share` included, as it already
did for `applications`. Only the old flat release layout writes to
`share/pixmaps`, so a missing `pixmaps` is still checked inside
`share` only, and a copy whose `share` the user can't write keeps
Update and Restart when every other release folder exists.
A folder in the app's folder or in `doc/<id>` that the user can't
read is still checked against its own permissions, as before. The
copy gets Download when the user can't write and search that folder.
The check can't list the folders inside it and does not check them.
A new test keeps this rule, and the check does not refuse a copy only
because a folder in it can't be read.
* craft-update: the staging cleanup's doc says which folders it removes
A marked staging folder is removed once its process has ended,
whichever app that install was for: the folder's name does not record
the app. Only staging folders of earlier launcher versions are checked
for the app's bundle.
* craft-update: a failed attach leaves its image attached when pgrep gives no answer
detach_unmounted, from #766, checked for another attach of the same
image file with is_running, which reads a pgrep that gives no answer as
"not running", and then force-detached the image's unmounted
attachments. It now uses process::running, from #753, and detaches only
when the check answers that nothing is running.
Moved from cadcraft-dxf (cadcraft 2f8f1861) unchanged apart from names and docs:
ASCII and binary DXF as group-code tags, sections and entities, pre-2007
code pages, versions, and writing. VectorCraft's hand-written DXF writer
can move onto it next.
Part of #778, #791.
Moved from lightcraft-tiff (lightcraft b188295e) unchanged apart from names
and docs. The first step of shared RAW support (#783, #587): LightCraft's raw
and metadata crates read through it, and PhotoCraft's and FilmCraft's own
TIFF parsing can follow.
Part of #778.
Moved from lightcraft-heif (lightcraft b188295e), the more complete of the
two copies: libheif-matching YCbCr conversion from the VUI, monochrome,
thumbnails and the box caps. PhotoCraft's grid-tile ICC inheritance tests
are ported to it.
Part of #778. Closes#784 once both apps switch.
render keeps a column map and a row map for each component while it writes
the output, but the check before allocating counted only the coefficients,
the reconstructed planes and the output. The maps are now counted too, at
their real element size.
* craft-print: ask macOS for Auto Select when a job names no tray
CUPS gives a job that names no tray the PPD's default tray. On a queue
whose default tray is empty (a Xerox AltaLink defaulting to Tray 1), the
printer holds every job ("Tray 1 is empty") while CUPS reports it
completed, so nothing prints. Preview prints because the macOS Print
dialog's Paper Feed: Auto Select sends AP_D_InputSlot with an empty value,
which drops the PPD's default tray (storytold/pdfcraft#3752).
- lp_args sends `-o AP_D_InputSlot=` on macOS when the job's options name
no InputSlot. A tray the job names wins; checked with cupsfilter on a
synthetic PPD: with both, CUPS emits the InputSlot's code.
- apply_queue_defaults (now shared with apps that read options
themselves, such as PdfCraft) gives the driver's tray option an
AUTO_SELECT choice, labelled "Auto Select", first and as the default,
so a dialog shows what lp_args sends. It is left out when the user's
lpoptions defaults name another tray, since lp adds that tray to every
job and it wins.
Linux jobs are unchanged: AP_D_InputSlot is Apple's.
* craft-print: match queue defaults to the PPD without regard to case
CUPS treats PPD option and choice names as case-insensitive. A queue
default spelled otherwise than the PPD (inputslot=tray2 for InputSlot
Tray2) was ignored, so on macOS apply_queue_defaults showed Auto Select
although lp would send Tray2. Keys and choices now match case-
insensitively and keep the PPD's spelling. lpoptions -l prints the PPD's
spelling, so this matters for callers passing defaults from elsewhere.
Review: storytold/craft-libs#232 (CodeRabbit).
* README: keep both the macOS tray and the Flatpak notes (the merge left the conflict markers in)
---------
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* media-testkit: shared test helpers and ffmpeg oracle discovery
* media-testkit: report fixture rename failure via skip; media: unlink relative craft-video path
* media-testkit: failed fixture generator honours CRAFT_MEDIA_REQUIRE_ORACLES
generate() returned None directly when the generator reported failure, so a
required-oracle run could pass with a missing fixture. Route it through skip,
like run_generator, and cover both policies with tests.
---------
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
storytold/pdfcraft#5806: Print in grayscale was refused ("the printer's driver did not retain
the requested grayscale; nothing was sent") on drivers that list no grey option the ticket can
name: Microsoft Print to PDF lists only Color, and an HP Photosmart driver has only its own
ns0000:JobHPPrintInGrayScale.
- Job::grey_pages: the caller has already made the pages grey. Grayscale is still asked of the
driver, but a driver that has no grey option, or doesn't keep it, prints the job instead of
refusing it. Off (the default), such a driver is refused as before, since the pages would
come out in colour.
- The ticket asks for psk:Grayscale when the driver lists that and not psk:Monochrome
(grey_option); Windows dropped the Monochrome it didn't list and colour came back.
- Offered::grey records whether PageOutputColor has a grey option at all.
printers() ran `lpstat -l -p -d` and `lpstat -e` with no time limit. When the CUPS scheduler
stops answering (macOS cupsd stopped serving its local socket in storytold/pdfcraft#6761), both
block forever and the Print dialog looks for printers until the app is closed.
Both now run through output_within (the bounded wait pdfcraft's lpoptions read already uses):
the queues get 15 s, network discovery 10 s. A scheduler that doesn't answer gives an empty list
and a warning naming cupsd; discovery that hangs only loses the driverless printers.
winprint.rs#19 draws each page at the resolution the job's ticket asks for instead of ~96 dpi
(storytold/pdfcraft#4941, #2676). #12 writes QName property values as prefix:local, which
drivers such as the Microsoft IPP Class Driver otherwise reject when merging the ticket
(storytold/pdfcraft#3823). #3 accepts whitespace around xsd:integer values in
PrintCapabilities (Toshiba Universal Printer 2, storytold/pdfcraft#2430).
The public API craft-print uses is unchanged; only the rev and the lockfile move.
printer_queues() (#618) lists the queues without asking any driver, so
every printer's two_sided is unknown there. A Print dialog listing from it
still needs to know whether the printer the user picks takes two-sided
settings, without asking every other driver as printers() does: Windows
takes about 48 s to give up on a printer it can't reach, such as a WSD
printer on another network (storytold/pdfcraft#2192).
two_sided(printer) finds that queue and reads its driver's capabilities
with the same check printers() makes, so the answers match. It is None
when there is no such queue, when the capabilities can't be read, and off
Windows, as printers() reports there.
The paused-printer test now checks that two_sided(name) gives printers()'s
answer for the test queue from exactly one driver query, and a new test
checks that a name no queue has is unknown. The query counting moves into
a small helper, which keeps windows_jobs_reach_the_spooler under the
cognitive-complexity limit.
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
spawn_detached now starts the process on its waiting thread, and the test
starts the helper on the calling thread. The comment names only what the
two share, standard streams set to null.
The warnings that open_url_with_fallback and the system opener log no
longer include the address being opened, so a link carrying a token or
personal details does not reach the application's log. The reasons from
the system stay in the messages.
On Unix, spawn_detached starts the waiting thread first, and that thread
starts the process and waits for it. When no thread can be started, the call
returns an error and the process is not started, so no exited process is
left unreaped. On Windows, where an exited process needs no wait, the
process starts on the calling thread and no thread waits for it. The list
of processes kept for a later call, and the reaping it needed, are removed.
When the opener of open_url_with_fallback is still running after its
timeout, the open-url thread waits for it. An opener whose state can't be
read is stopped and waited for.
The tests run spawn_detached's waiting-thread path and check that the
waiting thread gets the process's exit status, that the process is not
started when no thread can start, and that a program that cannot start
reports it. They no longer need ps.
A process that no thread could wait for is reaped when a later call starts
another process after it has exited. start_pending_update is called only as
the app exits: afterwards a finishing installation starts its helper itself
instead of reporting through tick, and one still running when the process
ends installs nothing. The reaping test waits for the process to exit
through its output and fails instead of looping when reaping does not
happen.
An app that calls start_pending_update in on_exit without dropping the
Updater now gets the same handoff as the drop: an install that finishes
after the call starts its helper itself instead of sending its outcome to
an Updater that will not read it.
When the update helper's step fails and starting the old app again fails
too, the error recorded for the next start names both, the step's first.
Before, the restart error replaced the step's.
When spawn_detached or the system opener cannot start a thread to wait for
the process it started, the process is kept and waited for by a later call
once it has exited, so on Unix it does not stay in the process table.
The Limits entry from 79f5839 said what goes wrong but not why it is left.
It now says that such a file is outside the MP4 specification: an MP4 AAC
sample is one raw access unit with the configuration in the sample
description (ISO/IEC 14496-14, 14496-3), so ADTS headers do not belong in
it, and a sample's duration is the time it decodes to (ISO/IEC 14496-12).
It says why files within the specification are not affected, what still
works, and why seeks and edits in such files are not corrected: finding
where each sample's audio starts means counting the ADTS frames in every
sample before it, one read per sample, which would slow every ADTS-framed
MP4 at open to work around files outside the specification.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AAC in MP4 stored as ADTS frames, with sample durations that count fewer
frames than the samples hold, decodes in full from the start, but seeks
place samples by those durations and land later than asked, and an edit
list measured in them can end the track early. The README's Limits section
now says so, and the seek example points to it.
Reported by FredApps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The test kept the events pipe open for reading and writing, so a helper
that exited before writing `started` left the read waiting forever. The
helper is now started through `relaunch_with` with a spawner the test
controls: it starts the helper the way `spawn_detached` does, and a
thread that waits for the helper to exit writes `exited`, so every read
gets a line. The test also checks that nothing is spawned before
`Restart::start`.
- On macOS and Linux the restart helper starts at the exit too
(Restart::start), instead of when the update is prepared. A close the
user cancels no longer leaves a helper that starts a second copy after
its one-minute wait, or a Flatpak helper that gives up before Restart
to Update.
- The record of a finished update names the release's version instead
of its tag, so a release whose tag is not a version is still reported.
- A standalone Windows copy updated from the portable zip becomes
portable. The helper now writes the outcome and the record of the
update to the folders beside it, which the new app reads.
- When the check at start finds a release for a prompt the app already
opened with Updater::offer, that prompt shows why the last update
failed.
- Errors and failure reasons reach 4.5:1 contrast on the card and in
the prompt's message box in both themes, and a long one scrolls inside
a 160 px area so the card's buttons stay on screen.
Corrects round 10. A short ADTS-framed MP4 track, whose last sample is in
the read-ahead, lost that sample's trim when it cut more than one frame:
the trim counted as durations falling short, so the length was left to
decoding and its padding was presented. The track's last sample is now left
out of the read-ahead comparison.
Reported by CodeRabbit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ADTS-framed AAC in MP4: durations short of the frames in the samples read
ahead no longer end the track. With HE-AAC samples of two ADTS frames each,
timed as two core frames, the durations matched one output frame per sample,
so the undercount check passed and the track stopped at half its length,
which probe reported as known. The frames each read-ahead sample holds are
now counted, and durations short of them leave the length unknown until
decoded, as durations short of one frame per sample already did. Correctly
timed AAC-LC samples of several frames keep their end trim.
Reported by FredApps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Windows helper tests hand step_after_wait a wait that reports when
the helper reaches it and blocks until the test has reaped the stand-in
launcher, so the old-file check no longer depends on a 500 ms sleep or a
2 s observation window. The test-only wait_watch hooks are removed. The
launcher-that-keeps-running test asks the real wait with no timeout
instead of bounding the elapsed time.
The Unix restart test puts a sleep script on the helper's PATH that
reports each pause through a named pipe, and the started command reports
whether the old process was still running. A thread that waits for the
helper to exit writes to the same pipe, so a helper that dies early fails
the test instead of hanging it.
A requested check still running when the user starts the installation from
the prompt answered with its own result once it finished, which replaced
Installing or Ready with UpToDate, the check error or nothing. Its answer is
now logged, and a requested check reports Installing, Ready or the recorded
helper failure, as a check started during the installation does.
Updater::offer opens a prompt that carries the failed update's reason, and now sets the status to Idle, as a check that finds a release does. An app that shows CheckResultDialog while the status is not Idle no longer shows the failure card and the prompt at once.
A new install worker after start_pending_update gets a fresh handoff flag, so
a retry after a helper that failed while the app kept running reports through
tick and starts its helper at the exit, instead of at once. Updater::offer
carries the reason of a failed update into its prompt, as a check does.
The outcome records are read only from plain files: a link is not followed,
as is_plain_file already does for the files it moves. The tests wait for a
check's answer on its channel instead of polling with sleeps.
start_pending_update now marks the Updater as exiting and lets go of an install
that is still running, so the worker starts its helper itself when it finishes
(or records its failure) instead of sending its outcome into a channel nobody
reads. Before, only dropping the Updater did this, so an app that called
start_pending_update in on_exit and kept the Updater alive lost the helper of
an install that finished afterwards. Drop now calls start_pending_update.
The docs of Action::Close and start_pending_update say that an app which ends
the process in on_exit loses an install still in progress.
restart_to_update only asks for the close; show_if returns Action::Close
only while an update is ready, so the test of a call with no update
ready fails when that check is removed. The test of a folder in place of
an outcome record is removed: Windows refuses to open a folder, so it
passed with the plain-file check removed.
start_pending_update now treats a disconnected install worker as a
failed update, as tick does, so the next start says the update stopped
unexpectedly.
A failed update's reason is kept until a prompt that offers the update
again shows it, so a dismissed failure followed by Check Again still
explains the failure. A prompt that already showed the reason does not
repeat it.
Windows.Data.Pdf draws a page on several of its own worker threads, which
share the print control's device context. With winprint 5f80b9b's
single-threaded factory Direct2D takes no locks, so those threads race in
d2d1.dll and the app closes while printing, most often on pages with Type 3
fonts (storytold/pdfcraft#5006, storytold/pdfcraft#2684).
storytold/winprint.rs#11 made the factory multi-threaded; it was merged as
64e7afd4.
craft-winprint-gdi moves to the same revision, so one checkout of
winprint.rs serves both; crates/gdi is unchanged between 417ac3c and
64e7afd4.
Co-authored-by: mertan-wq <235487900+mertan-wq@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
An installation that finishes after the Updater is dropped now records a
helper that cannot start, and a failed installation, so the next start
reports it. start_pending_update records an installation failure it picks
up at the exit too. When a requested check finds that a started helper
failed, the open prompt no longer offers Restart to Update: it says why
the update could not be installed, and no close is asked for.
A requested check while an update downloads or waits for the app to close
now reports Installing or Ready instead of leaving the result window
blank, and an app the launcher manages reports Managed instead of calling
its version the newest.
On Windows a prepared update says "Update ready. Close {app} to install
it." rather than that it is installed. The helper records the version it
installed just before it starts the new app, and the first start of that
version reports Updated. A recorded helper failure is reported once at the
next start (InstallFailed, with Check Again), in the prompt when the
update is offered again, and to a requested check while the old app still
runs. A failure wins over a stale record of a finished update, and a new
update clears both records. Records are read up to 4 KiB.
Action::Close is now a request: the Windows helper starts only at the exit
(dropping the Updater or start_pending_update in on_exit). A close the
user cancels keeps the update ready, and Restart to Update in the prompt
or the result window (Updater::restart_to_update) asks for the close
again. A helper that cannot start at the exit is recorded and reported at
the next start.
New strings for app catalogs: "Installing update…", "Update ready. Close
{app} to install it.", "Restart to Update", "The update could not be
installed: {reason}", "{app} updated to {version}." and "{app} {version}
follows the version chosen in the launcher, so it does not update itself."