A call that timed out left the app's request future running, so an app that releases a request when its future is dropped (the desktop ControlRequest abandons a request nobody waits for) ran the call late, after the page had been told it failed. call() now drives the request future itself and drops it when the timer wins.
The desktop control protocols name the base64 PNG of ui.screenshot differently (pngBase64, png_base64, png, base64), so screenshot() gave the page a different shape in each app. It now also puts the PNG in data and leaves the app's own field where it is.
The timer handle is kept as the environment returned it: Node returns an object, not a number, so a numeric id never cleared the timer and it fired after its closure was dropped.
The deny-list paragraph and the chrono calendar exception now live in the Rust rules. CLAUDE.md imports AGENTS.md, and the workspace-lints requirement stays with the new-crate notes.
* craft-print: a job can be cancelled until it starts reaching the spooler
On Windows a job asks the printer's driver for its capabilities before
anything is sent. A networked printer in power save can hold that query
for a long time, and an app had no way to give up on the job without
risking it printing later (storytold/wordcraft#5710; CodeRabbit on
storytold/wordcraft#5732).
A Cancel token, passed to the new submit_cancellable, moves a job from
pending to cancelled or to sending. The job checks it at the last point
before anything reaches the spooler: on Windows after the driver has
answered, before the job file is written; on macOS and Linux before lp
starts. A cancelled job returns an error and is never sent; a job already
being sent completes. submit and submit_with_progress are unchanged, and
Error keeps its one variant so callers that match on it still compile.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* craft-print: a job turns to sending with the provider held; the job file waits for it
Review: marked as sending before it took the PrintTicket provider, a job
could then wait behind another job's driver call that a sleeping printer
held, and an app that waits only for jobs being sent would wait on it.
The job now turns to sending only once it holds the provider, right
before printing, and writes its file only after that, so a cancelled job
leaves no copy of the document behind.
A test-only hold in the driver call lets the Windows spooler test cancel
a job while its driver doesn't answer: the cancel wins and, once the
driver answers, the job stops instead of reaching the paused queue.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* craft-print: the DEVMODE conversion comes before a job turns to sending
CodeRabbit on #622: winprint converts the ticket to a DEVMODE, a driver
call a sleeping printer can stall, before it creates the native job, so
a job stalled there had already turned to sending and couldn't be
cancelled. The conversion now runs first, with the provider held, while
the job can still be cancelled; the provider stays held until the first
copy's own conversion, so no other job comes between them.
Tests: the Windows spooler test also cancels a job held in that
conversion; the CUPS test gives a spooler that can't start, so any
attempt to start it would change the error. README: cancellation in the
progress section.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* craft-print: clarify cancellation during winprint conversion repeat
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* craft-font-catalog: run the Unix font-folder tests only on Unix
Five tests build an Os::Unix FontEnv from Unix paths (/home/me, <dir>/sys/fonts</dir>).
Windows doesn't treat those paths as absolute, so the tests failed there. Production code
is unaffected: on Windows FontEnv::from_process picks Os::Windows, whose folders never go
through the Unix path logic. FontEnv's docs now say its paths follow the host's path rules.
The font-store workflow, which already triggers on crates/font-catalog, now also runs
craft-font-catalog's clippy and tests on Linux and Windows.
Refs #619
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* craft-font-catalog: keep the store override and no-store tests on Windows
These assertions don't depend on Unix path rules, so they now sit in their own
ungated test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* craft-font-catalog: verify store override precedence
Exercise the override with platform defaults present on every OS variant.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The live spooler test only checked that printer_queues() reports
two_sided as unknown, which would still pass if it called printers() and
cleared the field (CodeRabbit on #618). A test-only per-thread counter in
two_sided(), the one PrintCapabilities query printer discovery makes,
now shows printers() querying the test queue's driver (the control) and
printer_queues() querying none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Windows, printers() reads each driver's PrintCapabilities to report
two-sided support before it returns. A networked printer in power save
can hold that query for a long time, so a Print dialog showed no printer
at all, not even the ones that were awake (storytold/wordcraft#5710,
storytold/wordcraft#5645, storytold/wordcraft#5413).
printer_queues() returns the same queues and default from EnumPrinters
and GetDefaultPrinter with two_sided unknown, and makes no driver call.
Submission still checks the job's options with the driver. On other
platforms it is printers(). printers() is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zip's `deflate` feature pulls in deflate-flate2-zlib-rs (and zopfli). Cargo
unifies flate2's features across the whole app, so on Windows every flate2
user in an app depending on craft-update switched to zlib-rs, whose level 1
is a static-Huffman quick mode that can produce output larger than its input
(Lightcraft's 16-bit ZIP TIFFs on Windows came out bigger than uncompressed).
Use `deflate-flate2` with flate2's default miniz_oxide backend instead; the
updater only reads Deflate zips, which that covers.
A blank name falls back to the default label only when no letter or
digit is left ("{name} 2" gives "2"); the docs and the rustdoc of
labels::named said a letter. A test pins both cases.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The accessibility section said a group's top border band is never
announced. Where no drag bar is drawn the band is the group's one move
handle and has the node.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep both craft-web-api and craft-web-host in the workspace list, the
README and the lockfile. Point the host README at the command API that
landed in #413, and the command API README at these host messages.
Co-authored-by: Cursor <cursoragent@cursor.com>
A page host can report a new dirty state while post_dirty's event is still
dispatching. Remember the state first so that later report wins, and restore
the previous state only when the send fails and nothing else recorded one.
Co-authored-by: Cursor <cursoragent@cursor.com>
A group's drag bar and its top border band were both buttons with
move_group_label, so a screen reader heard every group twice, and every
group and column bar had the same name. The band, which only drags, is
now out of the accessibility tree and keyboard focus (unless no bar is
drawn), and a `{name}` in move_group_label or move_column_label stands
for what the handle moves: the logical label of the panel the group
shows, or the titled panels a column's groups show, joined by ", ". A
blank title leaves the placeholder out with the brackets, quotes or
separator around it. Labels without it, today's defaults included, are
used unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wasm-bindgen throws "closure invoked recursively" when an FnMut closure
is called while it runs, before its body can queue the message. The
listeners only read their captures, so they are Fn; the handler's state
stays behind the Inbox's RefCell.
A page host dispatches its events synchronously, so it can answer (a
saved for the save the handler just posted) while the handler is still
running. The nested borrow_mut then panicked. Both transports now hand
messages to an Inbox that queues each one and drains the queue one call
at a time, in order; nothing is dropped. The new test fails with the old
borrow ("already borrowed").
FileData::size() reads a Blob's length from the Blob; a host's size
field is no longer trusted for an app's size check. lib.rs gets the
crate-level deny of unwrap, expect, panic and the like.
The messages between a Craft app's web build and the page that hosts it
(ready, host, open, save, saved, dirty, failed, handoff, handoff-done),
as proposed on #358. The contract part (message kinds, the version, the
?host=parent / data-host="page" switch, file-name clean-up) builds
everywhere and is tested natively; the web module (wasm32) is the bridge
an app's web shell calls: same-origin parent only, files as bytes or a
Blob without copying, saves as a transferred ArrayBuffer, dirty repeats
suppressed. Compatible with the bridges shipped before it: optional
fields stay optional, and a host treats an unknown ready.version as 1.
CI: the package repair between ffmpeg install tries gets a time limit
too, so a blocked configure script cannot use up the job's time.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI: time-limit and retry the ffmpeg install. A stalled apt mirror used
up the oracle job's 20 minutes on #442, #443 and #447 before any test
ran, and its log is only printed when the step fails. Each try now gets
3 minutes, up to three tries, and a timed-out install prints why.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Ogg track tests set their files' serial numbers instead of writing
the files until ffmpeg happens to pick the wanted order. They rewrite
each page's serial and checksum, so both orders run with one ffmpeg call
and do not depend on ffmpeg's serials being random.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Ogg track tests no longer depend on chance. Each writes its file
until the serial numbers are in the order it asks for, in both orders,
so the reversed order that told the old code apart always runs. A file
this ffmpeg cannot write goes through `skip`, which fails the test when
oracles are required.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A track index in an Ogg file means the same stream everywhere: the
file's order, as `tracks` lists them. Streams the native path does not
read fell back to symphonia with the same index, but symphonia orders
tracks by serial, which ffmpeg picks at random. A file could decode the
wrong track, or fail on a FLAC stream listed before an Opus one.
The tests write each file eight times, so both serial orders occur, and
cover a track with no mask comment next to one with a mask.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each FLAC stream in an Ogg file reads its own channel mask comment. The
tags symphonia's reader collects do not say which track they are from,
so a track could get another track's mask. The masks now come from
craft-media's Ogg demuxer, by serial, which is symphonia's track ID.
The Ogg layout test also covers 7 channels with no comment and a zero
mask.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FLAC in Ogg reads its layout the way a FLAC file does: from the mask
comment, or else the one decoders assume for the count. Without this, a
6-channel stream with no comment reported back surrounds in Ogg and side
surrounds in a FLAC file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- FLAC files read their layout from the channel mask comment, or else
the one decoders assume for the count, which is what the writer omits
the comment for
- an unbuilt format is Unsupported before the layout is checked
- FLAC encode no longer reserves half the PCM size up front
- flac-encode's lib tests stream a file and compare it with encode
- the seek test compares samples after the seek
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No change in output: the 74 oracle test streams are byte-identical.
- The Wasted case is handled in one function, without the unreachable arm
and the wrapper round 8 added.
- The bitstream writer uses predict::fold, the tested copy, not its own.
- STREAMINFO's MD5 may be 0 (RFC 9639 §8.2), so the comment no longer says
the RFC requires it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tests only; each was confirmed to fail on a mutated encoder.
- Every sample rate code in the frame header table.
- Signals that reach the Rice parameter and LPC shift limits, where a
value past the limit gives a file decoders refuse.
- ENCODE_EXTENSIONS lists flac.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refuse a linear predictor whose prediction leaves the i32 range. Such a
stream is valid, but symphonia adds the prediction to the residual as an
i32, which panics in a debug build. The round 8 residual limit made this
reachable at 24 bits; without the override, 13 of 240 clipped 32-bit test
streams failed in a debug build and none do now, for 0.09% more bytes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Code wasted bits (RFC 9639 §9.2.2): low bits that are zero in every
sample of a subframe were stored anyway, so 16-bit audio exported at 24
or 32 bits grew by the padding.
- Allow residuals up to the 32-bit limit (§9.2.7.3). The limit was half
that, so at 32 bits a transient forced its whole block to verbatim.
- README: 32-bit FLAC codes stereo channels independently.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Test the high bits of STREAMINFO's total sample count, and that a total
past 36 bits is left unknown.
- README limits: the usual 5- and 6-channel layouts depend on the mask
comment too; say what is outside the streamable subset.
- Comments: 32-bit code 0 does not decode everywhere and leaves the
subset; the oracle test helper is in craft_media_testkit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tests only, plus docs; the encoder does not change. Each new check was
confirmed to fail on a mutated encoder.
- Tests: frame numbers count up, LPC and Rice partitions and stereo
decorrelation save the space they should, FileEncoder records a FLAC
layout, the smallest frame anywhere reaches STREAMINFO, Writer::write
applies the frame limit, finish leaves the sink at the end, the exact
655350 Hz cap, the sample size codes.
- README limits: a FLAC layout decoders would not assume depends on its
mask comment, and ffmpeg and flac ignore a zero mask.
- The encode example's comment named no referent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- libFLAC and ffmpeg read a FLAC with 5 or 6 channels and no mask comment
as having side surrounds, so the comment is now written for anything
else, including the crate's usual back-surround layouts. The round 3
check treated back surrounds as FLAC's assignment, which lost them.
- Tests: the mask comment rules, opposite-polarity stereo, STREAMINFO
block and frame sizes.
- A stale comment said only WAV records the layout.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The PICTURE metadata test inserts its block without the last-block flag:
the encoder's VORBIS_COMMENT follows it, and the flagged version was a
broken metadata chain that only decoded by resyncing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FLAC files record the speaker layout when FLAC's channel assignment
does not already say it, as a WAVEFORMATEXTENSIBLE_CHANNEL_MASK comment.
FLAC's 7-channel order is 6.1 with side surrounds, so a buffer in the
crate's usual 7-channel layout (backs and a back centre) was decoded by
ffmpeg and libFLAC with three channels on the wrong speakers; explicit
layouts such as 5.1 with side surrounds were dropped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- check_room docs: FLAC holds at most 2^36 - 1 frames.
- The FLAC round trip covers every channel count from 1 to 8.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- FileEncoder/encode for FLAC hand the encoder one 4096-frame block at a
time: a whole-buffer encode drained the pending samples quadratically
(480 s of stereo took 19 s, now 5 s) and copied the whole input twice.
- Frame headers name sample rates outside the table (codes 12-14), so
streams at rates like 11025 or 37800 Hz stay in the streamable subset.
- The decode doctest needs symphonia to read FLAC at this point.
- README: FLAC output is verified against ffmpeg, not the flac tool.
- Tests: keep the AIFF force-RF64 check; exact 2^36 frame boundary.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FilmCraft's FLAC encoder behind `flac-encode`, kept over SoundCraft's:
linear prediction, stereo decorrelation and the STREAMINFO MD5 give
0.1-37% smaller files on 12 test signals, and the output is bit-exact
through `flac` and ffmpeg. It is 3-5x slower than SoundCraft's, still
about 56x real time.
`lossless-encode` is now `pcm-encode` plus `flac-encode`, and
`EncodeFormat::Flac` writes it through `encode` and `FileEncoder`.
With only one of the two features, the other's formats are refused as
`Unsupported`.
The tests that use FLAC files as decode fixtures (truncation,
corruption, metadata bounds, zero-frame files, container preflight)
come with it, as do the FLAC oracle tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* craft-audio: core types, WAV/AIFF read and write, peaks, symphonia
The new crate without any codec module: what every later codec plugs
into.
- Shared types: `AudioBuffer`, `AudioInfo`, `ChannelLayout`/`Speaker`,
`AudioError`, `EncodeOptions` (built with `new` and `with_*`),
`EncodeFormat`, `BitDepth`, `BwfInfo`, `SampleFormat`.
- WAV/BWF/RF64 and AIFF/AIFF-C reading and writing from SoundCraft's
`soundcraft-audio-io`, kept over EffectCraft's and FilmCraft's
smaller writers (only it has BWF, dither, any speaker mask and
streaming without a known length). `encode` and `FileEncoder` behind
`pcm-encode`; `lossless-encode` and `encode` turn it on.
- `decode`, `probe`, `tracks` and the streaming, seekable `Decoder`
(feature `decode-native`). The native path reads WAV, AIFF, and PCM in
MP4/MOV and Matroska through craft-media's demuxers, packet by packet,
with edit lists and `CodecDelay`/`DiscardPadding` applied.
`TrackDecoder` decodes PCM; each codec PR adds its codec to it.
- symphonia 0.6 (MPL-2.0, used unmodified) behind the `symphonia`
feature, part of `decode`: the fallback for every format with no
native decoder yet. Its size fields are checked before it reads, and
its panics are caught where they unwind.
- `peaks`: the min/max/RMS pyramid for waveform drawing.
Workspace: craft-media is a workspace dependency without default
features, so an encode-only craft-audio does not build the MP4
demuxer, and `symphonia-bundle-flac` builds without overflow checks in
dev profiles (its 32-bit LPC restore wraps on valid input).
CI: `audio.yml`, with clippy per feature, rustdoc, WASM, MSRV, and the
tests with ffmpeg oracles.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* craft-audio: review fixes (CodeRabbit on #441)
- Reading: AIFF that declares frames without an SSND chunk is Malformed; a Matroska PCM track's
length counts from its first frame; seek targets saturate; an empty FLAC has one track; a
failed header read is an Io error; deinterleave allocates fallibly; concealed silence is capped
like a decoded packet, and symphonia's uses the damaged packet's own duration when known.
- Peaks sum squares in f64, so loud float samples give a finite RMS.
- Writing: .aifc maps to AIFF; BWF text is cut at a character boundary; a WAV byte rate past
32 bits is an error; the FileEncoder docs say nothing may follow the sink's position.
- README: the Limits list says which codecs go through symphonia and which MP4 edit lists apply.
- Tests for each, alias and dither coverage, and a FLAC case in the routing oracle.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* craft-audio: review fixes, second round (CodeRabbit on #441)
- Every header read is checked: `Native::open`, `native::tracks` and the no-symphonia error read
it with `head_checked`. craft-media demuxer errors that are I/O failures (not a file ending
early) are returned as `AudioError::Io` instead of sending the file to the other path.
- A Matroska track's presented length runs from its first frame plus `CodecDelay` (unclamped,
in timestamp ticks with the delay rounded to one) to its end.
- symphonia's concealment allocates fallibly, and a damaged first packet is concealed when the
demuxer gives its duration. `deinterleave` reserves its channel list fallibly too.
- `TrackDecoder::conceal` documents its cap at the decode limit.
- Tests: an I/O failure past the first 64 KiB, codec delay with the first block at -20, 0 and
50 ms (checked with `probe`), a seek back after an overshoot, and the loud-sample RMS value.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* craft-audio: TrackDecoder::conceal returns a Result
Concealment allocates fallibly and fails with TooLarge when the silence would pass the 2^30-sample
decode limit, instead of returning less than the packet's length. The native path, the README,
the packets example and the doc example pass the error on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* craft-audio: review fixes, third round (CodeRabbit on #441)
- Concealed silence is held to the decode limit inside pcm::silence, so symphonia's path checks
frames × channels too, and symphonia's decoded audio is copied into fallibly allocated planes.
- TrackDecoder::reset forgets the last packet's length.
- Without symphonia, a damaged MP4, Matroska or Ogg file (truncated, invalid, no moov) is
Malformed instead of Unsupported; a feature the demuxer does not read still names the build.
- Tests: an MP4 with no moov in a native-only build, conceal after a reset and past the limit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* craft-audio: review fixes, fourth round (CodeRabbit on #441)
- A file the native demuxers find damaged is Malformed with symphonia too, when symphonia calls
it unsupported; before, an ftyp-only MP4 was Unsupported in symphonia builds.
- symphonia's decoded blocks are held to the 2^30-sample decode limit before allocating.
- Symph::seek forgets the last packet's length, so a lost packet after a seek is not concealed
with the length of one before it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* craft-audio: review fixes, fifth round (CodeRabbit on #441)
- The damaged-file test covers Matroska as well as MP4, and fails when either branch of
native::damage is removed.
- native::damage leaves out Ogg: ogg::open reports no damage (it skips damaged pages and calls a
file with none it can read not Ogg), and symphonia reports damaged Ogg as malformed itself.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* craft-audio: run the ffmpeg oracle tests on ubuntu-26.04
ubuntu-latest's apt ships ffmpeg 6.1.1, which writes MP2 and E-AC-3 in
Matroska with ends a few milliseconds early, so the routing tests on the
later codec PRs cannot pass on it. ubuntu-26.04 ships ffmpeg 8.0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Nav kept the rows of the last frame that drew them. When a title click
closed the popup before its rows drew again, Enter or Space ran the
cached command with its old enabled state. Nav::load now clears the rows,
so the keys act only on rows drawn this frame under the current gate.
- docking: apply_with_permissions refuses a move or open into a floating
window for a panel that is not floatable (Move, OpenAt, MovePanel and
group/column/window moves), and a Group placement at a floating anchor,
which becomes a split of its frame, needs accepts_splits.
- fonts: the served-font loader keys fetched files, in-flight requests,
failures and pending UI faces by (url, integrity), and installs bytes
only into faces requiring the integrity they were fetched under
(Stack::provide_fetched); the public Stack::provide is unchanged.