* Packaging: pin the Windows signing client and check its hash
* Packaging: pin appimagetool and check its hash
* Fail clearly when no appimagetool is pinned for the architecture
* Pin the AppImage runtime
ci.yml calls the shared workflow (cached apt packages, nextest, main-only cache saves, Cargo.lock check). FreeBSD and Windows ARM64 run nightly and on pushes to release instead of on pull requests. Depends on storytold/craft-common-actions#69.
* Build: line tables only for workspace crates, no debug info for dependencies
* Tests: one integration-test binary per crate
* xtask ci: nextest, doctests, test build without examples, one wasm check
* Use chrono for UTC/calendar conversion instead of hand-rolled copies
Replaces the hand-rolled civil_from_days / civil copies and the hand-written
weekday / leap-year / days-in-month helpers with chrono. chrono is already a
workspace dependency (used by crates/text), so this reuses that entry and just
adds `chrono = { workspace = true }` to the engine and desktop app rather than
a second version.
- apps/deckcraft logging `timestamp` now formats through chrono (private
civil_from_days deleted).
- engine cmd::design `civil` (days since 1970 -> date, used for comment
timestamps in review.rs) uses chrono::NaiveDate.
- text datetime `DateTime::is_valid` / `weekday` use chrono::NaiveDate instead
of a hand-rolled leap-year table and days-since-1970 weekday.
Output is byte-identical and pinned by the existing logging, design and
datetime-field tests; each converted helper keeps a proleptic-Gregorian
fallback for dates outside chrono's NaiveDate range.
Follow-up to storytold/cadcraft#1785
* text: make chrono available on wasm32
is_valid/weekday in crates/text/src/datetime.rs use chrono unconditionally,
but chrono was only declared under the non-wasm target, so the crate (and
engine/pdf/render/ui-egui depending on it) failed the wasm32-unknown-unknown
check in `cargo xtask ci`.
- crates/text: declare chrono unconditionally (wasm-safe `std` baseline),
and keep `clock` in a separate cfg(not(target_arch="wasm32")) entry for
now_local()'s local clock, which is already wasm-gated to return None.
- workspace: drop `clock` from the shared chrono entry so it is no longer
forced onto wasm builds (engine uses chrono unconditionally too); `clock`
is now requested per-crate only where the local clock is read.
* Never panic on a clock outside chrono's range; harden the civil fallback
logging.rs timestamp(): chrono's From<SystemTime> unwraps, so a clock past
year 262143 panicked inside the logger. Use the fallible DateTime::from_timestamp
and read an out-of-range or pre-epoch clock as the epoch, as the old saturating
code did. Years past 9999 are written with {:04} like before, because %Y adds a
'+'. Tests cover both cases.
cmd/design.rs civil_fallback(): make the hand-rolled fallback's year arithmetic
saturating so civil() cannot overflow or panic at i64::MAX/MIN (the engine rule
forbids crashes and unchecked arithmetic). Behaviour is unchanged for every real
input; a test exercises both extremes. The output year is formatted with {y:04}
by review.rs, so no %Y prefix issue.
crates/text/src/datetime.rs: checked after review; is_valid already limited the
year to 1..=9999 before this PR, weekday() is only reached for valid dates, and
all its arithmetic is checked/euclidean, so no change is needed.
* Don't build an unrepresentable SystemTime in the far-future test
UNIX_EPOCH + Duration::from_secs(u64::MAX / 2) overflows Windows'
SystemTime and panicked before timestamp() ran, failing the Windows
test job. Build it with checked_add and only assert where it exists.
* Make civil() exact for every i64 instead of saturating
The out-of-range fallback saturated z + 719_468, so civil(i64::MAX)
returned a date 719,468 days early. civil() now has chrono convert the day
within its 400-year Gregorian cycle and adds whole cycles back as years
(i128 for the extremes), so the hand-rolled fallback is gone. Exact results
for i64::MAX/MIN and both sides of chrono's range are tested against Hinnant's formula.
* time: preserve out-of-range years in the log timestamp instead of the epoch
`DateTime::from_timestamp(..).unwrap_or_default()` changed the existing output: a
clock set far ahead (an input the old hand-rolled civil-from-days converted)
collapsed to `1970-01-01`. Restore the old conversion domain with a cycle-based
helper: reduce by whole 400-year Gregorian cycles (146_097 days), convert the
in-range remainder with chrono, and carry the year back as i128. Only the year
leaves chrono's range; month/day/time stay exact. Pre-1970 still clamps to the
epoch as before. chrono stays the only calendar logic (no new features).
apps/deckcraft logging.rs `timestamp` (SystemTime, u64 secs), pinned via a
platform-safe `UNIX_EPOCH.checked_add`:
u64::MAX/2 s → "292277026596-12-04T15:30:07.000Z" (was "1970-01-01..."),
10e12 s → "318857-05-20T17:46:40.000Z", 253_402_300_800 s → "10000-01-01...".
* Name the 400-year range extension as the one exception to the time rule
The rule said never to hand-roll calendar math, but the range fix
(shifting by whole 400-year Gregorian cycles so dates past chrono's year
range keep their old output) is cycle arithmetic, so the rule and the code
disagreed. The rule now names that exception: the library still computes
month, day and time, and only whole cycles are carried in the year.
* Say the library computes month and day; time-of-day is plain arithmetic
Narrows the range-extension wording: the date library computes the
calendar fields, and splitting seconds into hours, minutes and seconds
is ordinary arithmetic, which is what the code does.
Use [[ ]] for conditional tests in the bash packaging scripts, give positional
parameters a name before use, end the shared helpers in common.sh with an explicit
return (bare return keeps the last command's status), and give the FreeBSD arch
case a no-op default. No behaviour change.
show.start {from} turned a negative or fractional index into slide 0 and an index past the end into a show that opened on the end screen, over the command and the control channel alike. Both now refuse it, as the slide commands do since #1063; a hidden slide still plays.
- The pane picks the first effect of the selected shape that is on the click sequence, so a
triggered effect before it no longer turns the button back into Play All; a clicked triggered
row falls back the same way.
- animation.preview rejects a from that is not an effect index instead of playing all.
- target-app-parity: the Animation Pane timeline is no longer a shortfall.
With an effect selected the pane's play button reads Play From and the
canvas preview starts at that effect on the play-all clock, so the
effects before it have already played. With nothing selected it is
still Play All. animation.preview takes an optional from index.
The backdrop cache knew its document by address only: after an edit
during the show, a new copy could reuse the freed address and get the
old backdrop. The cache now holds that document and matches on it.
(CodeRabbit review on #1161.)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follow-up to #1011, as asked on #1038: a counter of thumbnails actually
rendered, and tests that use it.
`Textures::thumb_count` is the number of thumbnails *held*. It settles at the
rows in view whether the pane rendered them once or a thousand times, so it
cannot see a pane that renders the same rows over and over -- which is what
#969 was: the flashing white was the rendering, not the holding.
`Textures::thumb_renders` counts the renders, and a settled pane is one that
has stopped asking for them.
The two new tests assert what the held count cannot:
- every thumbnail held was rendered, and not many more than that were rendered
and thrown away (measured: 8 held, 8 rendered at rest);
- ten more frames of the same view render nothing at all;
- after a 40,000 pt scroll of an 800-slide deck, 24 renders in total -- the
rows it landed on and the ones it passed over, not the deck -- and then it
settles again.
With the cache made to thrash (its 600-entry limit cut to 4, so the rows in
view are evicted and re-rendered every frame), the merged test for the Slide
Sorter still passes -- the held count settles at 4 -- while this one fails with
"255 renders for 4 thumbnails". That is the hole the counter closes.