814 Commits

Author SHA1 Message Date
FredApps ea1fbeb05b Merge pull request #470 from KotovGGG/codex/docking-host-geometry
Add opt-in docking host surfaces and fitted resize chrome
2026-11-02 18:26:01 +01:00
Aleksander Helgaker 006b50d2ae craft-media: shared codec identifiers, one ByteSource, the testkit (1/10) (#439)
* craft-media: shared codec identifiers, one ByteSource, the testkit

The types a codec crate needs to describe a file and configure a
decoder, ahead of craft-audio:

- `Container`, `CodecId`, `PcmFormat` and `CodecParams` (`id.rs`),
  always built, with optional serde.
- `codec_params()` on `isobmff::Track` and `matroska::Track`, both
  returning `CodecParams` in the same form; `extradata` follows
  Matroska's `CodecPrivate`.
- One `ByteSource` trait (`source.rs`), with `ReadSeekSource`;
  `isobmff::ByteSource` and `matroska::ByteSource` are that trait.
- craft-media-testkit (`publish = false`): the ffmpeg oracle helpers
  from craft-media's tests, so the codec crates share one copy.

The existing tests prove the refactor; the new types have their own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* craft-media: APV extradata is the bare record; reseek after a poisoned lock

An MP4 apvC track gave CodecParams.extradata with the 4-byte FullBox header
in front, where Matroska's V_APV CodecPrivate is the bare
APVDecoderConfigurationRecord. ApvConfig::record_bytes serializes the record
alone and extradata uses it.

ReadSeekSource kept its tracked position after recovering a poisoned lock or
a failed seek, so a reader moved by a panic was read from the wrong offset
with no error. Both now force a seek on the next read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* craft-media: MP4 codec params per sample description

Track::codec_params always described the first stsd entry, while each sample
selects its own through description_index. It now describes the first
sample's entry; codec_params_for gives the parameters of any entry, and
has_mixed_descriptions says whether one set covers every sample.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* craft-media: test file-backed ByteSources; a File read past i64::MAX is an EOF

A File and a ReadSeekSource over a file now have the buffer's test: the same
bytes at the same offsets, and UnexpectedEof past the end. The test found
that a File read at an offset past i64::MAX failed with the OS's
InvalidInput instead of the UnexpectedEof the trait promises; it now
reports the end of the source like the other implementations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* craft-media: Matroska codec params match MP4 for ALAC, DNx and Opus

Matroska's codec_params gave Unknown for A_ALAC and for DNxHD/DNxHR stored
as V_MS/VFW/FOURCC (AVdn, AVdh), where MP4 gives Alac and Dnxhd; ALAC's
extradata is the CodecPrivate, the ALACSpecificConfig MP4's alac box holds.
Opus now reports 48 kHz from Matroska as from MP4, whatever rate the muxer
declared.

ReadSeekSource clears the poison when it recovers the lock, so one
panicked read costs one recovery instead of a seek on every later read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* craft-media: MP4 reports the MPEG audio layer the first frame names

An MP4 track with MPEG audio reported CodecId::Mp3 whatever the layer, since
mp4a + 0x69/0x6B names none (ffmpeg stores MP2 that way), while Matroska
reports Mp1/Mp2/Mp3 from its CodecID. open() now reads the frame header of
each such entry's first sample and codec_params() reports the layer it
names; without a valid header, QuickTime's .mp2 gives Mp2 and anything else
Mp3 as before. CodecConfig::Mp3 is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 14:18:36 +01:00
.CooCoo b8f7d93b00 Add craft-pcraft-format: the envelope of PhotoCraft's .pcraft bundle (#407)
* Add craft-pcraft-format: the envelope of PhotoCraft's .pcraft bundle

The manifest around the document, with the document as a type parameter
(Manifest<D>), TileRef, the bundle's entry paths and hash check,
FORMAT_VERSION, the manifest decode limits with the nesting and item
scan, and the migration runner as a generic step list. Messages and
serialized field order are PhotoCraft's, so its saved manifests stay
byte-identical when it switches to these types.

Part of #59.

* ci: do not keep the checkout token in the pcraft-format workflow

The checkout step sets persist-credentials: false, as jpeg.yml and media.yml do. No later step in the job uses the token.

* pcraft-format: build no entry name from an invalid hash

tile_path and blob_path return None unless the hash is 64 lowercase hex
digits, so a hash read from a file such as "../x" never becomes an entry
name. TileRef::path gives a tile's entry the same way.

The docs note that MAX_GROUP_DEPTH is a copy of PhotoCraft's constant,
and that check_manifest also refuses a manifest larger than
max_manifest_bytes, which PhotoCraft's check before a save does not.

* ci: pin actions/checkout by commit and Rust to 1.95 in the pcraft-format workflow

* pcraft-format: read_manifest is the one reader of a manifest

read_manifest(json, limits, steps, target) checks the JSON against the
limits, refuses a format_version newer than target, runs the migration
steps, and reads the document as D and the other envelope members.
read_manifest_with reads the document with the caller's function
instead. Manifest no longer implements Deserialize.

Error::TooNew holds the manifest's generator, so a message can name the
writer; its message is unchanged. TooNew, Manifest and TileRef are
#[non_exhaustive], and Manifest::new and TileRef::new build them.

TILES_DIR, BLOBS_DIR, COMPOSITE_DIR and OBJECT_SUFFIX name the bundle's
directories and the object suffix. The docs of max_blob_bytes and
max_total_bytes say the caller enforces them.

* pcraft-format: read_manifest reports errors in serde's struct order

read_manifest reads the envelope members and the document in the order
the parsed object holds them, and reports a missing member after the
members that are present. A manifest with several faults gives the same
error as a derived Deserialize of the whole manifest, as PhotoCraft
reads it today.

* pcraft-format: document that a missing or invalid format_version is Corrupt

read_manifest's docs and the README say that a missing or invalid
format_version gives Error::Corrupt from migrate, and that a missing
generator or document, or any other mistyped member, gives Error::Json.
A test covers a string and a fractional format_version.

* pcraft-format: read_manifest refuses preview names other than its own

A manifest's `thumbnail` and `composite` named any entry the reader then
opened, while tile entries go through `is_valid_hash`. `read_manifest`
now gives `Error::Corrupt` for a `thumbnail` other than `thumb.png` or a
`composite` other than `composite/preview.png`, the only names PhotoCraft
writes. The check runs after the members are read, so JSON errors keep
their order.

The reader's doc now says a missing or invalid `format_version` is
`Error::Corrupt`, from `migrate`, not `Error::Json`.
2026-11-01 23:31:04 -05:00
.CooCoo 14d5dbb50c Add craft-vectorcraft-format: the .vectorcraft envelope, with the document as a type parameter (#408)
* Add craft-vectorcraft-format: the .vectorcraft envelope, with the document as a type parameter

File<D>, Head and Image hold the members VectorCraft writes, in its
order, with its skip rules: serde_json output of a File<D> is the JSON
VectorCraft writes, compact or pretty. Tail places entries after the
JSON from version 4 (INLINE_MAX bytes or more, after BLOB_MAGIC), and
split_tail and Image::bytes read them back. The crate also has the
version and name constants, check() for the format name and newer
versions, sniff() for plain and gzip files (the top-level "format"
member must be vectorcraft or drawcraft), and standard base64.

No dependency on any app; builds for wasm32-unknown-unknown.

* ci: do not keep the checkout token in the vectorcraft-format workflow

The checkout step sets persist-credentials: false, as jpeg.yml and media.yml do. No later step in the job uses the token.

* vectorcraft-format: limit the bytes copied from the data after the JSON

Entries::bytes reads a file's entries and returns Error::TooBig once the
blob ranges it has copied add up to more than the data after the JSON
holds, so entries that name the same bytes again and again are refused,
as VectorCraft's reader refuses them.

A generator, preview or pdf of the wrong shape now reads as None in File
and Head, as VectorCraft opens such a file. File::tail gives a Tail for
the file's own version.

* ci: pin actions/checkout by commit and Rust to 1.95 in the vectorcraft-format workflow

* vectorcraft-format: document that base64 decode ignores the bits before the padding

decode reads a group such as Zh== like Zg==, and Zm9= like Zm8=, as
VectorCraft's own base64_decode does. The doc comment says so, and a
test pins both cases to the canonical bytes.

* vectorcraft-format: entries naming the same range share one buffer

VectorCraft's reader keeps the bytes of each [offset, length] it has
read and gives a repeated entry the same buffer without counting it
again (crates/format/src/unpack.rs). Entries::bytes refused such a
file once the repeats passed the size of the data after the JSON. It
now returns Arc<[u8]>, caches each range, and counts it once.
Overlapping ranges that differ are still copied and counted each time.

* vectorcraft-format: a refused entry does not count against the copy limit

Entries::bytes added a refused entry's length to the bytes copied, so a later entry that fit was refused too. It now counts an entry only once it is read.

* vectorcraft-format: Contents reads the document without keeping the preview and PDF

File<D> checks the shape of generator, preview and pdf by holding each
value in memory, and keeps the preview and PDF base64 it reads. Contents<D>
reads format, version, profiles, document and images, and skips the other
three as the JSON is read. It accepts and refuses the same files as File<D>.

* vectorcraft-format: Contents reads a repeated generator, preview or pdf

VectorCraft opens a file whose generator, preview or pdf member is repeated, since its reader does not declare those members, and refuses a repeated format, version, profiles, document or images. Contents now leaves the three members undeclared, so serde skips every copy of them, and still refuses the other repeated members. File and Head are unchanged.

* vectorcraft-format: sniff decodes JSON escapes in the format member

serde_json reads a file whose format key or value is written with escapes, such as \u0066ormat, as a VectorCraft file, and sniff compared the raw bytes and refused it. sniff now decodes the key and the value before comparing them.

* vectorcraft-format: File::read and Contents::read check the version

File::read and Contents::read are the readers of a file's JSON. Both
refuse another format or a version newer than VERSION after the JSON
reads, with ReadError::Envelope holding the Error (Format or TooNew), and
return ReadError::Json otherwise. File and Contents no longer implement
Deserialize, so a reader cannot skip the check.

Both read one implementation and treat repeated members the same way:
a repeated generator, preview or pdf is accepted, and File keeps the last
value, as serde_json reads a repeated key into a Value. A repeated
format, version, document, images or profiles is refused. A top-level
array holds format, version, document, images and profiles in that
order, as VectorCraft reads it; Contents' fields follow that order.

- Image::bytes returns None for an entry of more than MAX_ENTRY (1 GiB,
  VectorCraft's limit on one entry of a compressed file), and
  Image::bytes_at_most takes another limit. Entries keeps no per-entry
  limit, as VectorCraft opens larger entries in uncompressed files.
- Entries::blob reads a range; Entries returns Arc<Vec<u8>>.
- marker_at finds BLOB_MAGIC where the writer puts it in a file's first
  bytes; split_tail uses it.
- File, Contents, Head, Image, Part and ReadError are #[non_exhaustive].
2026-11-01 19:57:07 -05:00
.CooCoo ca1b5e2edb Add craft-ecproj-format: the envelope of EffectCraft project files (#409)
* Add craft-ecproj-format: the envelope of EffectCraft project files

An .ecproj file is one JSON object: savedBy, then the saved view's
members, then the project's members, written with two-space indentation.
The crate defines that envelope with the view and the project as type
parameters, so the project model stays in EffectCraft:

- SCHEMA_VERSION (2) and check_schema, which refuses a newer schema
  with EffectCraft's own message.
- File { saved_by, view, project }, serialized with both parts
  flattened, and File::to_json.
- Head, which reads savedBy and schema from a JSON object, and
  saved_by, which reads savedBy alone.

The tests write synthetic projects and compare the text with JSON
written by hand in the shape EffectCraft's writer gives. The workflow
runs fmt, clippy, the tests and a wasm32-unknown-unknown check.

* ci: do not keep the checkout token in the ecproj-format workflow

The checkout step sets persist-credentials: false, as jpeg.yml and media.yml do. No later step in the job uses the token.

* ecproj-format: write compact files, read savedBy as EffectCraft opens it

EffectCraft's Save Compact Project Files setting (effectcraft 07639b5f)
writes .ecproj files on one line with serde_json::to_string.
File::to_json_compact writes the same text, and the docs say the crate
writes and reads both forms.

Head now reads every file EffectCraft's Project::from_json opens: a
savedBy that is not a string, or appears more than once, reads as None,
and a project written as a JSON array reads with its first element as
the schema. saved_by also gives None for a repeated savedBy, as
EffectCraft's saved_by does. File's docs say the crate does not check
values and that serde_json writes NaN and infinity as null.

* ecproj-format: skip a non-string savedBy without keeping it

Head::read and saved_by read savedBy into a serde_json::Value before
dropping any value that is not a string, so a wide or deeply nested
savedBy was held in memory, and one nested past serde_json's recursion
limit made Head::read fail. A non-string savedBy, and every repeated
savedBy, is now skipped element by element with IgnoredAny, as other
members are. It still reads as None.

* ecproj-format: Head::read refuses a newer schema and reads objects only

Head::read is the one reader of a project file's envelope. It returns
ReadError: Json when the text is not one JSON object with a u32 schema
(the array form included), or TooNew when the schema is newer than
SCHEMA_VERSION, holding the file's savedBy so a message can name the
version that wrote it. TooNew reads as before. saved_by() is removed,
and Head no longer implements Deserialize.

TooNew, ReadError and ReadError::TooNew are #[non_exhaustive].
2026-11-01 19:56:26 -05:00
Dylan M. Taylor f3b62ec61f Merge pull request #472 from rwoldberg/crw-stream-end-check
crw: refuse an entropy stream cut short
2026-11-01 12:04:25 -05:00
Dylan M. Taylor 45326724d7 test(crw): add stream-end fill boundary and recovery tests 2026-11-01 12:03:49 -05:00
KotovGGG 71e5ecf285 test(ui): verify floating frame compatibility and resize constraints 2026-11-01 12:39:28 +03:00
aiongg f8f8829cc7 Merge pull request #463 from dotCooCoo/fix/keymap-intl-backslash-key
keymap: the ISO key next to left Shift can be bound to shortcuts
2026-10-31 23:42:00 -04:00
aiongg 970fd6fa39 keymap: test the UK ISO key; the Turkish Q item applies to Windows and Linux 2026-11-01 03:31:51 +00:00
.CooCoo fb08be5fa1 krilla: CMYK, gray and spot colors keep their exact inks (patch 17) (#417)
* krilla: CMYK, gray and spot colors keep their exact inks (patch 17)

Gray, CMYK, Separation and DeviceN colors stored each component as a
u8, so every ink was written as a multiple of 1/255: C75 became
0.7490196 and C4 M4 Y4 K4 became 0.039215688.

- cmyk, luma, separation and device_n Color::new_f32 take components
  from 0 to 1 and write them as given. The u8 constructors are
  unchanged.
- Gradient stops rewritten into one DeviceN, Separation or CMYK space
  keep their components.
- Mesh gradients write corner colors at 16 bits a component.

* Tests: the 75% K mesh corner comment gives the asserted 0.749996
2026-10-31 23:30:23 -04:00
.CooCoo 9d17829194 hayro-syntax: skip a dictionary entry whose value does not parse (#165)
Dict validates a dictionary by skipping its values, and the skip accepts
some values that the parser cannot read: a name with a broken # escape
(/#zz), a number such as 12-.5, and a dictionary followed by stream with
no endstream. Dict::entries and Dict's Debug output unwrapped the result
of parsing every value, so either one panicked on such a dictionary.

Port LaurenzV/hayro#1501 to the vendored copy: entries skips an entry
whose value does not parse, and Debug prints the entries. ArrayIter::next
already ends the iteration on such an item here. Add LaurenzV/hayro#1501's
two tests, update the local hashes of dict.rs and array.rs and the repair
list in SOURCE.json, and note the change in vendor/README.craft-reader.md.
2026-10-31 23:30:03 -04:00
.CooCoo 69c268014e craft-print: serialize calls to the Windows PrintTicket provider (#171)
The first time a process opens a PrintTicket provider for a queue whose
provider is PrintConfig.dll, winspool.drv creates a process-wide cache
without a lock. When two threads make those first calls together,
winspool can keep a pointer to a cache it has already freed, and a later
PTOpenProvider call crashes the process with an access violation in
ntdll. Listing printers from several threads at once reaches it through
two_sided.

Hold one process-wide lock around every call that reaches the provider:
PrintCapabilities::fetch in two_sided and driver_ticket, the
PrintTicketBuilder in driver_ticket, fetch_xml_for_ticket in
imageable_margin, and the start of print_with_progress, where winprint
converts the ticket to a DEVMODE. The job releases the lock at its first
progress report, before it sends progress to the submitting thread. A
lock poisoned by a panic is still taken.
2026-10-31 23:29:47 -04:00
RWoldberg ab2bcccf84 crw: refuse an entropy stream cut short
When the entropy stream ends before the frame does, the stream-end rule gives the
remaining samples their predictor values. That was silent: a raw-data record cut
short inside an intact CIFF heap decoded as a smeared image instead of an error.

`decode` now counts those samples (`Image::stream_end_fill`) and returns
`Error::Corrupt` when there are more than `Limits::max_stream_end_fill` (default
64, one block). `Limits` is non-exhaustive, so the field is additive. The frozen
format is unchanged: a fill within the limit decodes exactly as before.

Every real file measured fills none: 1,318 files of all seven verified bodies
(1,308 EOS D60), whose last samples come from an end-of-block code. The CC0 sample
test still decodes all ten files exactly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 17:22:59 -06:00
KotovGGG 3685bae898 fix(ui): suppress trailing drops for host-owned groups 2026-11-01 02:09:38 +03:00
KotovGGG 9a9755568f test(ui): enumerate dock edges without unreachable branches 2026-11-01 01:45:06 +03:00
KotovGGG 4f263edcdd fix(ui): omit stock body bands for host-owned dock groups 2026-11-01 01:31:53 +03:00
KotovGGG 235fffbdbe fix(ui): preserve host docking geometry and split ownership 2026-11-01 00:02:55 +03:00
KotovGGG dcd94c27e7 feat(ui): add host-owned docking surfaces and fitted resize chrome 2026-10-31 18:57:49 +03:00
dotCooCoo fc37b20235 keymap: the ISO key next to left Shift can be bound to shortcuts
craft_keymap::Key gains IntlBackslash, the key between left Shift and Z
on ISO keyboards (`<` and `>` on Turkish Q). Key::from_egui and to_egui
map it to egui's IntlBackslash, so the shortcut recorder records it and
the dispatcher runs its bindings. Its shortcut text is `IntlBackslash`
(`Cmd+IntlBackslash`, `Shift+IntlBackslash`), read without regard to
case; `<` and `>` still parse as no key.

The key counts as a symbol position: Shift stays in the chord, and once
the layout has typed `<` there, display_for shows `Ctrl+<`.
MAX_CHORD_TEXT goes from 64 to 72 bytes so that the longest canonical
trigger, `Cmd+Ctrl+Alt+Shift+IntlBackslash` pressed twice, reads back.
2026-10-30 23:00:40 -07:00
Dylan M. Taylor c39e435cc3 Merge pull request #462 from aiongg/update-result-dialog
update: CheckResultDialog for the result of a manual update check
2026-10-31 01:04:55 -04:00
Dylan M. Taylor 487b4d4270 Merge pull request #461 from aiongg/docs-tidy
docs: tidy AGENTS.md and clean-room doc
2026-10-31 01:04:43 -04:00
aiongg c5a1cbfb5f update: CheckResultDialog shows the result of a manual update check 2026-10-31 04:39:40 +00:00
aiongg a2be97cf8e docs: tidy AGENTS.md and clean-room doc 2026-10-31 04:17:35 +00:00
Dylan M. Taylor 4323a0f90e Merge pull request #406 from solarmaster961/fix/flatpak-system-bundle-self-update
fix(linux): self-update a system-wide Flatpak bundle in place
2026-10-30 22:56:43 -04:00
Dylan M. Taylor 003f133f6d Update crates/update/src/install.rs
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-10-30 22:48:52 -04:00
VoiceLessQ 7ec82661cb fix(update): cap a GitHub rate-limit wait at one hour
An absurd x-ratelimit-reset or retry-after stopped the API until restart.
GitHub's quota refills within the hour.
2026-10-30 22:44:18 -04:00
VoiceLessQ f0b5b22dce fix(update): read GitHub API answers up to a 16 MiB cap
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.
2026-10-30 22:43:46 -04:00
Dylan M. Taylor 2b8bd856ce Merge pull request #458 from storytold/fixup/crw-365-follow-up
fixup(crw): trim derivation artifacts (follow-up to #365)
2026-10-30 22:42:50 -04:00
Dylan M. Taylor a0fdc73717 fixup(crw): trim shipped derivation artifacts 2026-10-30 22:41:55 -04:00
Dylan M. Taylor b645a09174 Merge pull request #365 from rwoldberg/clean-room-crw-format-decode
Clean room crw format decode
2026-10-30 22:40:13 -04:00
Dylan M. Taylor 0ad3952f5e docs(crw): declare table generator Python version 2026-10-30 22:40:07 -04:00
Dylan M. Taylor 8846e5d34a Merge pull request #449 from Jeremy8776/feat/update-version-pins
craft-update: apps respect the launcher's per-app version choice
2026-10-30 20:55:18 -04:00
Jeremy 9817c81fd2 craft-update: the version-choice file sits in a host folder Flatpaks can read
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>
2026-10-30 21:55:00 +00:00
Jeremy 0ada678488 craft-update: every check answer uses the launcher's choice as it stands when the check ends
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>
2026-10-30 12:52:57 +00:00
Jeremy a710ab7897 craft-update: a pin made while a check runs still holds its answer
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>
2026-10-30 12:42:23 +00:00
Jeremy 0db8c1ba47 craft-update: a pin closes a stale prompt; tracks() skips stable; isolate the pretend test
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>
2026-10-30 12:36:42 +00:00
Jeremy 7d303b4b50 craft-update: apps respect the launcher's per-app version choice
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>
2026-10-30 12:22:38 +00:00
Dylan M. Taylor 04aa1703b3 update: honor pretend versions in app release checks (#436) 2026-10-30 02:12:36 -04:00
Dylan M. Taylor 1065653e75 craft-update: the startup prompt shows the app's icon, accent colour and developer (#434)
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 }.
2026-10-30 01:25:19 -04:00
Dylan M. Taylor 733a038aa5 craft-print: log why the printer list is empty when lpstat can't run; document Flatpak needs (#429)
* 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.
2026-10-29 18:48:26 -04:00
aiongg 1b5582a72e Merge pull request #423 from Jeremy8776/feat/self-update-migrate-to-machine
craft-update: an app installed for one account moves to an install for everyone on its update
2026-10-29 11:44:02 -04:00
aiongg 488c1da8fe Merge pull request #425 from Jeremy8776/fix/update-local-build-downgrade
craft-update: a build from source is never offered the release it already contains
2026-10-29 11:35:24 -04:00
Jeremy 8efdc23ce8 Merge upstream/main to resolve MSI migration conflicts 2026-10-29 15:32:14 +00:00
Jeremy df399edff8 craft-update: a Windows MSI copy updates itself in its own context, wherever it is installed (#419)
* 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>
2026-10-29 11:26:20 -04:00
Jeremy 25c2e9ffc5 craft-update: resolve the whole default folders, not just LOCALAPPDATA
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>
2026-10-29 13:50:36 +00:00
Jeremy eff2500447 craft-update: migration review fixes (profile links, recovery message, Linux test)
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>
2026-10-29 13:33:30 +00:00
Jeremy 7fd60c5870 craft-update: a build from source is never offered the release it already contains
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>
2026-10-29 13:29:47 +00:00
Jeremy 2e2e8f8daa craft-update: an app installed for one account moves to an install for everyone on its update
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>
2026-10-29 13:14:18 +00:00
Jeremy cd7cc8eb39 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>
2026-10-29 12:51:39 +00:00