Commit Graph
159 Commits
Author SHA1 Message Date
Mex c20e3e0ac2 fix(platform): select compatible macOS DMGs for Intel Macs (#414) 2026-10-17 14:17:58 -06:00
Mex e4bb3e6972 fix(download): stop retrying when an oversized partial cannot be removed (#495) 2026-10-17 14:17:48 -06:00
Mex c5caa024d6 fix(update): reject invalid self-update marker timestamps (#492) 2026-10-17 14:16:51 -06:00
Dylan M. Taylor 721e960bda feat(catalog): align app catalog ordering with getartcraft.com, drop AppGroup, and standardize nix/freebsd workflows (#507) 2026-10-17 13:42:02 -06:00
307efcf45a feat(install): choose where Craft apps are installed (#461)
* feat(install): choose where Craft apps are installed

Settings gets a default install folder, and a fresh install asks for the
app's folder (prefilled with the default). Updates stay beside the copy
they replace. Works for MSI (INSTALLFOLDER), DMG bundles, and AppImages;
on Linux a custom folder means AppImage. Flatpak and the web build keep
the system location and skip the prompt.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(windows): integrate portable installs with custom locations

* fix(install): ask before writing over something already in the install folder

Staging used a predictable `.<name>.partial` path beside the destination, so a
file the user kept under that name could be overwritten (AppImage) or deleted
(macOS). Stage in a fresh private folder instead.

A new install whose destination already exists now stops before downloading
and asks "Replace what's there?". Replace installs over it: the AppImage or
.app is swapped in, a portable install writes over same-named files only, and
the MSI installs into the existing folder. Cancel leaves everything as it was.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test(install): a planted .partial symlink can't redirect AppImage staging

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Skyfuller1303 <[email protected]>
2026-10-15 20:46:32 -06:00
Dylan M. Taylor 3fd2d56999 fix(update): a Flatpak-installed launcher gets the bundle download link, not the AppImage (#370)
Co-Authored-From: dylan
Co-Stream: [email protected]
2026-10-14 21:40:31 -06:00
Mex 0f295d4b9c fix(install): preserve unrelated update backups
Reserve an exclusively owned backup directory while replacing an installed app. Preserve neighboring .old files and directories, and retain the old copy with its recovery path if rollback fails.

Includes regression coverage for backup collisions and failed rollback.

PR #332.
2026-10-13 23:40:50 -06:00
Skyfuller 1579aa8c02 fix(ui): sharpen Windows launcher rendering
Present Windows DX12 through DirectComposition while retaining the presentation-system override and existing graphics selection.

Use whole-pixel glyph placement for the bundled fonts and inset the card hover rule within its hairlines.

Refs #339.
2026-10-13 17:18:31 -06:00
Brandon ThomasandClaude Opus 5.5 cbae640688 Add FigCraft; release 0.4.1 (#313)
* Add FigCraft to the launcher

FigCraft (storytold/figcraft): interface design and prototyping, the
thirteenth Craft App and the sixth in the New group.

- Catalog row (brand colour #02da96 from FigCraft's icon README), details
  page (no pictures yet), Linux desktop categories, icon tables, tagline
  in every catalog, xtask local mirror list.
- Icon copied unmodified from FigCraft's repo; ATTRIBUTION.md row.
- The Windows installer art fits five rows of icons.
- Test that a companion figcraft-server tarball is never picked as the app.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Release 0.4.1

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
v0.4.1
2026-10-13 12:28:29 -04:00
Brandon ThomasandClaude Opus 5.5 d6fdab6596 Release 0.4.0 (#291)
Co-authored-by: Claude Opus 5.5 <[email protected]>
v0.4.0
2026-10-13 06:29:23 -04:00
Brandon ThomasandClaude Opus 5.5 1aaf7b27be AGENTS.md: agents never land or approve third-party apps (#213)
A catalog row, download source or install path for an app outside storytold/, or a rule change
that would allow one, is for a maintainer to decide. Agents flag it and stop.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 05:41:18 -04:00
Brandon ThomasandClaude Opus 5.5 2b86d4ca52 Point the details pictures at storytold/craft-launcher-assets (#221)
The pictures repo was renamed from artcraft-launcher-assets to match craft-launcher. GitHub
redirects the old name for now, but the launcher should not depend on that.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 05:02:23 -04:00
AlexisandBrandon Thomas cb82e851cd Add a details drawer for each Craft App (#60)
* Add a hand-written details page for each Craft App

core::details holds, for every app in the suite, a headline, a few
paragraphs, a list of features, where it runs and the list of its
pictures. The copy is the launcher's own: written from each app's page on
getartcraft.com and, for the five newer apps, from its README. It is
compiled in, so it works offline and in the web build, and it leaves out
figures that go stale (effect counts, timings).

The pictures are not in this repository. A page names them (a key and a
caption each) and AppDetails::screenshot_url turns a key into the address
of a 1200 px JPEG in storytold/artcraft-launcher-assets, pinned to a tag
(SCREENSHOTS_REV) so a saved copy never goes stale. It only answers for
keys the page lists, so no address is ever built from anyone else's text.
MAX_SCREENSHOT_BYTES caps what is accepted, and looks_like_jpeg refuses a
login page or error page that arrives as a 200.

PdfCraft lists none yet: its screenshots still show its old name
(PrintCraft). ATTRIBUTION.md points to the pictures repository, which
carries each picture's own provenance.

Tests check that every app has exactly one page, that each is filled in,
and that addresses come only from a page's own pictures.

* Open an app's details in a drawer from its card

Clicking a card (or an app's name in the table) slides a drawer in from
the right over the dimmed grid. It shows the app's headline, version and
main button, links to its website, GitHub and releases, a gallery of
screenshots, its features, the facts and notes of its latest release, and
the same trademark notice as the grid. Esc, the close button or a click
on the grid closes it. The page's text is the hand-written copy in
core::details; the version, date, download size and notes come from the
release the launcher already fetches.

The pictures are fetched when a page is opened, not bundled. The UI sends
Command::FetchScreenshots and gets one Event::Screenshot per picture, the
lead one first. The desktop backend downloads them through the launcher's
Http (its User-Agent, a 2 MB cap, redirects followed) and keeps them under
<cache>/screenshots/<revision>/<app>/<key>.jpg, so later opens, and any
time offline, read from disk. Only a JPEG is kept: a login page or error
page that arrives as a 200 is refused. A missing picture doesn't hold the
others back, but once the network has failed the rest fail at once instead
of each waiting for its own timeout. The web build does the same with
fetch and leaves the caching to the browser.

Until a picture is here, a frame of the same size says "Loading pictures…"
or, when it failed, that it couldn't be loaded and that reopening the page
tries again; nothing moves when the picture arrives. The lead picture
opens full-window, the arrows over the pictures and the arrow keys flip
through them, wrapping around, and a scroll bar stays visible (egui's
floating one only shows under the pointer).

The drawer's labels are tl! strings, with 13 rows in each of ru, zh-CN,
de, fr and pt-BR; details.rs joins the sources the catalog coverage test
reads. Every size taken from the window is kept non-negative: the new
tests resize it from 40 x 30 to 4000 x 2400 with the drawer open.

actions() becomes actions_in(), which can leave the links out of the
... menu where the page already shows them as buttons. The image crate's
jpeg feature is enabled to decode the pictures (zune-jpeg and zune-core
join Cargo.lock). The dim and the enlarged picture's backdrop are colours
in theme.rs.

* Bring the details drawer up to date with main

- Website is always a button now (#198); drop the "Like {name}" line and the trademark notice,
  which main removed with the product comparisons (93caf01).
- Bring back widgets::eyebrow for the drawer's section headings (#187 dropped its last caller).
- The drawer's ⋯ menu leaves out only the links, so Hide/Unhide (#185) stays.
- No slide when the OS asks for reduced motion (#201).
- Translate the screen readers' "{name} details" label, and add the drawer's strings to the
  Spanish, Czech, Polish, Hungarian and Japanese catalogs.
- AGENTS.md: name the pictures' download origin.

---------

Co-authored-by: Brandon Thomas <[email protected]>
2026-10-12 05:00:22 -04:00
0e87e90aef macOS DMG: branded window and hdiutil create, so the app passes codesign --strict (#169)
* macOS: build the DMG with hdiutil create so the app passes codesign --strict

hdiutil makehybrid -hfs put a com.apple.FinderInfo on every file in the
image, so the signed app inside the DMG (and any ditto copy of it) failed
codesign --verify --deep --strict. That is why verify.sh had dropped
--strict. Build the image with hdiutil create -srcfolder (retried, since
create can hit "Resource busy" on CI), clear build-time xattrs before
signing, and make verify.sh check the app in the finished DMG and a ditto
copy of it with --strict again.

Ported from storytold/pdfcraft#823 and storytold/lightcraft#959.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* macOS DMG: branded window background and icon layout

The launcher's DMG opened as a plain Finder window, unlike every other
Craft app. Add packaging/macos/dmg/ the same way WordCraft and PhotoCraft
did: a background (the ArtCraft mark whole on its blue, title, Ink and
Paper), a .DS_Store with the window size and icon positions, and the
generate.py that writes both reproducibly. The volume is now named
"ArtCraft Launcher" without the version, which the background alias needs;
the DMG file name keeps the version.

Fixes #20

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Fabio Lussi <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 04:26:54 -04:00
78ef4a8c77 Linux: draw on the desktop's GPU, not always the discrete one (#164)
* Linux: draw on the desktop's GPU, not always the discrete one

egui asks wgpu for the high-performance adapter. On a hybrid laptop in
PRIME on-demand mode (Intel + NVIDIA, X11, GNOME) that is the NVIDIA GPU
while the display server runs on the Intel one, so every frame is copied
between the GPUs. During a resize the window then shows stale or garbled
frames and can stop drawing until it is resized again.

On Linux and the BSDs, ask for no power preference instead: wgpu then
takes the first adapter Vulkan lists, and Mesa's device-select layer lists
the display server's GPU first (or the one DRI_PRIME picks). Measured on
an RTX 4060 + Intel RPL-S laptop: in on-demand mode the list starts with
the Intel GPU, in `prime-select nvidia` mode with the NVIDIA one, so the
launcher draws on whichever GPU drives the screens. WGPU_POWER_PREF still
overrides it. Windows and macOS keep their current choice.

Refs #69

* Linux GPU: keep wgpu's adapter order, but a software rasterizer last

Without Mesa's device-select layer the Vulkan order follows the ICD files, and lavapipe can come
before the real driver.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 02:50:10 -04:00
1c03e2d2ea Accessibility/UI: follow the OS's animation setting (#201)
* Add a Polish interface

- pl.tsv: the full catalog (287 rows), written clean-room from the English
  labels; plurals use the same one|few|many rule as Russian.
- Registered as the seventh language: "Polski" in the picker, locales pl,
  pl-PL and pl_PL.UTF-8, complete_menus = true so the completeness tests
  cover it.
- tests/polish.rs: the Polish UI test, modeled on the French one.
- README and AGENTS updated for the new language.

* Add Hungarian localization and fix responsive settings layout

* i18n: add Czech (cs) interface language

Czech catalog with every key of the most complete catalog (287 rows), the
cs language entry with locale detection (cs-CZ, cs_CZ.UTF-8, ?lang=cs),
a Czech plural rule (1 / 2-4 / other) with a unit test, and the README
language list.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* i18n: add Japanese (ja) interface language

ja.tsv translates the whole interface, the tray menu and tooltip, each app's
tagline and the backend's install/update messages into Japanese, and
Settings > Language offers 日本語. Japanese locales (ja, ja-JP, ja_JP.UTF-8…)
select it; Japanese has one plural form.

Japanese is a CJK language, so cjk_font.rs now looks for a Japanese system
font (Hiragino Sans on macOS; Yu Gothic, Meiryo or MS Gothic on Windows;
Noto Sans CJK JP, Noto Sans JP, IPAGothic, Takao or VL Gothic on Linux and
FreeBSD), falling back to a Chinese one, which draws kana and most kanji,
where the system has none. The font is loaded per language, so switching
between Japanese and Chinese reloads the right face.

A fallback face is nudged onto the body font's baseline: egui puts a fallback
glyph's baseline at that face's own ascent and only centres the two line
boxes, which left a face whose metrics differ from Inter's riding high. On
Windows, Yu Gothic's line gap is half an em, so its kana and kanji sat about
0.29 em above the Latin text beside them (Microsoft YaHei, used for Chinese,
happens to match). The nudge, applied through a FontTweak, is the difference
in ascent and line height, and helps the Chinese faces too.

The README and AGENTS list the new language. Tests: the complete-catalog
checks cover ja.tsv (placeholders, plural forms, every tl! literal, taglines,
package formats), plus a locale/plural test, a Japanese glyph-coverage test,
a baseline test and tests/japanese.rs (grid, table, dates, a backend message).

* i18n: make the CJK font choice language-neutral

The CJK comments, the missing-font note and the plural-rule docs named the
languages, so each new one meant editing them all; they now say "CJK". Each
language's writing system (its `script`) now lives in the registry, and a
language is drawn only with a face for its own script — never another's,
which would leave characters as tofu — so a language the system has no font
for stays in English, like any other. Adding Korean or Traditional Chinese is
a registry row and a candidate list, not a special case.

* Hide Craft apps the user is not interested in.

* Read the Linux desktop appearance portal so system theme matching works on Wayland.

* UI: follow the OS's animation setting

With Windows' "Animation effects" switched off, the launcher still glided each
notch of the mouse wheel over several frames and faded its dropdowns in. egui
does both by default, and nothing asked the OS.

The shell now says what the OS wants (Host::reduce_motion) and the UI asks it
about once a second while it draws, so a change shows without a restart. With
fewer animations asked for:

- a mouse wheel scrolls at once. egui has no switch for its wheel smoothing but
  applies a trackpad's small steps as they come, so each wheel event is
  rewritten as a run of those in the shell's raw_input_hook;
- dropdowns, tooltips and dialogs appear without a fade, and scrolling to a
  place jumps there (style.animation_time, style.scroll_animation);
- the rule along the top of a hovered card appears instead of wiping in.

Progress bars and spinners keep moving: they say that work is going on.

Where the setting comes from:

- Windows: SPI_GETCLIENTAREAANIMATION, read as bit 33 of UserPreferencesMask in
  the registry, as SystemParametersInfoW needs unsafe;
- macOS: NSWorkspace's accessibilityDisplayShouldReduceMotion;
- Linux and the BSDs: the XDG settings portal over D-Bus (its reduced-motion,
  GNOME's enable-animations, KDE's AnimationDurationFactor), followed on a
  thread so the UI never waits for D-Bus;
- web: prefers-reduced-motion.

Animations stay on wherever the setting can't be read. winreg and zbus become
direct dependencies of the desktop app; both were already in Cargo.lock.

The Windows path was run on a machine with the setting off. The macOS and Linux
paths were type-checked for those targets but never run, and the web one only
compiled.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* UI tests: hovering a card follows the OS's animation setting

With animations off, moving the pointer onto a card leaves the window still
within 3 frames: its rule and its icon's rise show at once. With them on, the
0.3 s wipe takes 19 frames at 60 frames a second. Each test fails when the
card's timing is made to ignore the setting.

The test window no longer has a download in progress, so it is still unless
something animates.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* i18n(pl): use Polish plural rule, so 21 is "21 aplikacji"

plural_east_slavic treats 21, 31 and 101 as "one", which is right for
Russian but not Polish, where only 1 itself takes the singular.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* system theme: back off to every 5 minutes when there's no appearance portal

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Merge #189 and main into #201: one raw_input_hook, and test anchors on taglines

Main no longer shows the "Like …" product lines, so the tests find cards by their taglines.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* i18n(cs): translate "Installed · latest {latest}" (added by #195)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* i18n(pl): translate "Installed · latest {latest}" (added by #195)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* i18n(hu): translate "Installed · latest {latest}" (added by #195)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* i18n(ja): translate "Installed · latest {latest}" (added by #195)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* i18n: hide/unhide strings for the Spanish, Czech, Polish, Hungarian and Japanese catalogs

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* hungarian test: count only the settings body's links, not the cards' Website links behind the modal

#198 gave every card a Website link, and Hungarian labels both Webhely.

* hungarian test: card links are 14px text, settings icons 24px; filter on that

---------

Co-authored-by: DzikStar <[email protected]>
Co-authored-by: ferivoq <[email protected]>
Co-authored-by: Losminek <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: leonaci <[email protected]>
Co-authored-by: satchmo91 <[email protected]>
Co-authored-by: Vikram Bhaskaran <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
2026-10-12 02:49:59 -04:00
cf573b2d35 Fix system theme matching on Linux Wayland (#189)
* Read the Linux desktop appearance portal so system theme matching works on Wayland.

* system theme: back off to every 5 minutes when there's no appearance portal

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 02:49:47 -04:00
250cc54335 Hide Craft apps the user is not interested in. (#185)
* Add a Polish interface

- pl.tsv: the full catalog (287 rows), written clean-room from the English
  labels; plurals use the same one|few|many rule as Russian.
- Registered as the seventh language: "Polski" in the picker, locales pl,
  pl-PL and pl_PL.UTF-8, complete_menus = true so the completeness tests
  cover it.
- tests/polish.rs: the Polish UI test, modeled on the French one.
- README and AGENTS updated for the new language.

* Add Hungarian localization and fix responsive settings layout

* i18n: add Czech (cs) interface language

Czech catalog with every key of the most complete catalog (287 rows), the
cs language entry with locale detection (cs-CZ, cs_CZ.UTF-8, ?lang=cs),
a Czech plural rule (1 / 2-4 / other) with a unit test, and the README
language list.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* i18n: add Japanese (ja) interface language

ja.tsv translates the whole interface, the tray menu and tooltip, each app's
tagline and the backend's install/update messages into Japanese, and
Settings > Language offers 日本語. Japanese locales (ja, ja-JP, ja_JP.UTF-8…)
select it; Japanese has one plural form.

Japanese is a CJK language, so cjk_font.rs now looks for a Japanese system
font (Hiragino Sans on macOS; Yu Gothic, Meiryo or MS Gothic on Windows;
Noto Sans CJK JP, Noto Sans JP, IPAGothic, Takao or VL Gothic on Linux and
FreeBSD), falling back to a Chinese one, which draws kana and most kanji,
where the system has none. The font is loaded per language, so switching
between Japanese and Chinese reloads the right face.

A fallback face is nudged onto the body font's baseline: egui puts a fallback
glyph's baseline at that face's own ascent and only centres the two line
boxes, which left a face whose metrics differ from Inter's riding high. On
Windows, Yu Gothic's line gap is half an em, so its kana and kanji sat about
0.29 em above the Latin text beside them (Microsoft YaHei, used for Chinese,
happens to match). The nudge, applied through a FontTweak, is the difference
in ascent and line height, and helps the Chinese faces too.

The README and AGENTS list the new language. Tests: the complete-catalog
checks cover ja.tsv (placeholders, plural forms, every tl! literal, taglines,
package formats), plus a locale/plural test, a Japanese glyph-coverage test,
a baseline test and tests/japanese.rs (grid, table, dates, a backend message).

* i18n: make the CJK font choice language-neutral

The CJK comments, the missing-font note and the plural-rule docs named the
languages, so each new one meant editing them all; they now say "CJK". Each
language's writing system (its `script`) now lives in the registry, and a
language is drawn only with a face for its own script — never another's,
which would leave characters as tofu — so a language the system has no font
for stays in English, like any other. Adding Korean or Traditional Chinese is
a registry row and a candidate list, not a special case.

* Hide Craft apps the user is not interested in.

* i18n(pl): use Polish plural rule, so 21 is "21 aplikacji"

plural_east_slavic treats 21, 31 and 101 as "one", which is right for
Russian but not Polish, where only 1 itself takes the singular.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* i18n(cs): translate "Installed · latest {latest}" (added by #195)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* i18n(pl): translate "Installed · latest {latest}" (added by #195)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* i18n(hu): translate "Installed · latest {latest}" (added by #195)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* i18n(ja): translate "Installed · latest {latest}" (added by #195)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* i18n: hide/unhide strings for the Spanish, Czech, Polish, Hungarian and Japanese catalogs

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* hungarian test: count only the settings body's links, not the cards' Website links behind the modal

#198 gave every card a Website link, and Hungarian labels both Webhely.

* hungarian test: card links are 14px text, settings icons 24px; filter on that

---------

Co-authored-by: DzikStar <[email protected]>
Co-authored-by: ferivoq <[email protected]>
Co-authored-by: Losminek <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: leonaci <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
2026-10-12 02:48:47 -04:00
93f12a5d9e i18n: add Japanese (ja) interface language (#177)
* Add a Polish interface

- pl.tsv: the full catalog (287 rows), written clean-room from the English
  labels; plurals use the same one|few|many rule as Russian.
- Registered as the seventh language: "Polski" in the picker, locales pl,
  pl-PL and pl_PL.UTF-8, complete_menus = true so the completeness tests
  cover it.
- tests/polish.rs: the Polish UI test, modeled on the French one.
- README and AGENTS updated for the new language.

* Add Hungarian localization and fix responsive settings layout

* i18n: add Czech (cs) interface language

Czech catalog with every key of the most complete catalog (287 rows), the
cs language entry with locale detection (cs-CZ, cs_CZ.UTF-8, ?lang=cs),
a Czech plural rule (1 / 2-4 / other) with a unit test, and the README
language list.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* i18n: add Japanese (ja) interface language

ja.tsv translates the whole interface, the tray menu and tooltip, each app's
tagline and the backend's install/update messages into Japanese, and
Settings > Language offers 日本語. Japanese locales (ja, ja-JP, ja_JP.UTF-8…)
select it; Japanese has one plural form.

Japanese is a CJK language, so cjk_font.rs now looks for a Japanese system
font (Hiragino Sans on macOS; Yu Gothic, Meiryo or MS Gothic on Windows;
Noto Sans CJK JP, Noto Sans JP, IPAGothic, Takao or VL Gothic on Linux and
FreeBSD), falling back to a Chinese one, which draws kana and most kanji,
where the system has none. The font is loaded per language, so switching
between Japanese and Chinese reloads the right face.

A fallback face is nudged onto the body font's baseline: egui puts a fallback
glyph's baseline at that face's own ascent and only centres the two line
boxes, which left a face whose metrics differ from Inter's riding high. On
Windows, Yu Gothic's line gap is half an em, so its kana and kanji sat about
0.29 em above the Latin text beside them (Microsoft YaHei, used for Chinese,
happens to match). The nudge, applied through a FontTweak, is the difference
in ascent and line height, and helps the Chinese faces too.

The README and AGENTS list the new language. Tests: the complete-catalog
checks cover ja.tsv (placeholders, plural forms, every tl! literal, taglines,
package formats), plus a locale/plural test, a Japanese glyph-coverage test,
a baseline test and tests/japanese.rs (grid, table, dates, a backend message).

* i18n: make the CJK font choice language-neutral

The CJK comments, the missing-font note and the plural-rule docs named the
languages, so each new one meant editing them all; they now say "CJK". Each
language's writing system (its `script`) now lives in the registry, and a
language is drawn only with a face for its own script — never another's,
which would leave characters as tofu — so a language the system has no font
for stays in English, like any other. Adding Korean or Traditional Chinese is
a registry row and a candidate list, not a special case.

* i18n(pl): use Polish plural rule, so 21 is "21 aplikacji"

plural_east_slavic treats 21, 31 and 101 as "one", which is right for
Russian but not Polish, where only 1 itself takes the singular.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* i18n(cs): translate "Installed · latest {latest}" (added by #195)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* i18n(pl): translate "Installed · latest {latest}" (added by #195)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* i18n(hu): translate "Installed · latest {latest}" (added by #195)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* i18n(ja): translate "Installed · latest {latest}" (added by #195)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* hungarian test: count only the settings body's links, not the cards' Website links behind the modal

#198 gave every card a Website link, and Hungarian labels both Webhely.

* hungarian test: card links are 14px text, settings icons 24px; filter on that

---------

Co-authored-by: DzikStar <[email protected]>
Co-authored-by: ferivoq <[email protected]>
Co-authored-by: Losminek <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
2026-10-12 02:48:34 -04:00
57105f8445 Add Hungarian localization and fix responsive settings layout (#146)
* Add a Polish interface

- pl.tsv: the full catalog (287 rows), written clean-room from the English
  labels; plurals use the same one|few|many rule as Russian.
- Registered as the seventh language: "Polski" in the picker, locales pl,
  pl-PL and pl_PL.UTF-8, complete_menus = true so the completeness tests
  cover it.
- tests/polish.rs: the Polish UI test, modeled on the French one.
- README and AGENTS updated for the new language.

* Add Hungarian localization and fix responsive settings layout

* i18n: add Czech (cs) interface language

Czech catalog with every key of the most complete catalog (287 rows), the
cs language entry with locale detection (cs-CZ, cs_CZ.UTF-8, ?lang=cs),
a Czech plural rule (1 / 2-4 / other) with a unit test, and the README
language list.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* i18n(pl): use Polish plural rule, so 21 is "21 aplikacji"

plural_east_slavic treats 21, 31 and 101 as "one", which is right for
Russian but not Polish, where only 1 itself takes the singular.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* i18n(cs): translate "Installed · latest {latest}" (added by #195)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* i18n(pl): translate "Installed · latest {latest}" (added by #195)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* i18n(hu): translate "Installed · latest {latest}" (added by #195)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* hungarian test: count only the settings body's links, not the cards' Website links behind the modal

#198 gave every card a Website link, and Hungarian labels both Webhely.

* hungarian test: card links are 14px text, settings icons 24px; filter on that

---------

Co-authored-by: DzikStar <[email protected]>
Co-authored-by: Losminek <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
2026-10-12 02:48:21 -04:00
94d6480a11 Add Polish (Polski) as an interface language. (#138)
* Add a Polish interface

- pl.tsv: the full catalog (287 rows), written clean-room from the English
  labels; plurals use the same one|few|many rule as Russian.
- Registered as the seventh language: "Polski" in the picker, locales pl,
  pl-PL and pl_PL.UTF-8, complete_menus = true so the completeness tests
  cover it.
- tests/polish.rs: the Polish UI test, modeled on the French one.
- README and AGENTS updated for the new language.

* i18n: add Czech (cs) interface language

Czech catalog with every key of the most complete catalog (287 rows), the
cs language entry with locale detection (cs-CZ, cs_CZ.UTF-8, ?lang=cs),
a Czech plural rule (1 / 2-4 / other) with a unit test, and the README
language list.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* i18n(pl): use Polish plural rule, so 21 is "21 aplikacji"

plural_east_slavic treats 21, 31 and 101 as "one", which is right for
Russian but not Polish, where only 1 itself takes the singular.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* i18n(cs): translate "Installed · latest {latest}" (added by #195)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* i18n(pl): translate "Installed · latest {latest}" (added by #195)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Losminek <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
2026-10-12 02:48:08 -04:00
6d59370024 i18n: add Czech (cs) interface language (#167)
* i18n: add Czech (cs) interface language

Czech catalog with every key of the most complete catalog (287 rows), the
cs language entry with locale detection (cs-CZ, cs_CZ.UTF-8, ?lang=cs),
a Czech plural rule (1 / 2-4 / other) with a unit test, and the README
language list.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* i18n(cs): translate "Installed · latest {latest}" (added by #195)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Losminek <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
2026-10-12 02:45:12 -04:00
Brandon ThomasandClaude Opus 5.5 a464e432b3 i18n(es): translate "Installed · latest {latest}" (#211)
#195 added the string and #196 marked Spanish complete; they landed together, so the
completeness test failed on main.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 02:02:14 -04:00
d4c61ec110 Improve indeterminate installer progress bar animation (#163)
* Improve indeterminate progress bar animation

Refactor indeterminate progress bar rendering for smoother animation.

* progress: rustfmt, and skip the wrapped segment while it's off the track

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 01:57:06 -04:00
155d6bd6b0 Detect PdfCraft installs made by its PrintCraft-named MSI (#134)
* Match MSI entries and find app executables through pure functions

Windows detection decided which uninstall entry belongs to an app, and
which paths might hold its exe, inline in the registry-reading code
under #[cfg(windows)]. Neither decision needs the registry or the disk,
so move both into module-level functions, is_installer_entry and
exe_candidates, that tests can call on every platform. The windows
module keeps the registry reads and the exists() checks.

No behaviour change: an entry still has to carry the app's display name
and the Learning Machines LLC publisher, exactly, and the candidates are
the same paths in the same order. The new tests pin that down for every
app but PdfCraft, and for PdfCraft's own entry name.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Detect PdfCraft installs made by its PrintCraft-named MSI

PdfCraft's Windows installer shipped as PrintCraft up to 0.2.1
(printcraft-0.2.1-windows-x64.msi). That MSI registers its uninstall
entry with DisplayName "PrintCraft" and installs printcraft.exe into
Program Files\PrintCraft, but Windows detection only accepted an entry
named after the app's current name and looked only for pdfcraft.exe, in
App Paths, the install folder and Program Files\PdfCraft. On a machine
still on that install the launcher showed PdfCraft as not installed,
with no version, and uninstall_msi found nothing to remove.

The catalog already knows the old asset stem (former_ids); add
former_names for the display names an app's installers registered,
since the capitalisation can't be derived from the stem, and names()
to go with stems(). Entry matching now accepts any of the app's names,
still exactly and still only from Learning Machines LLC, and the exe is
looked up under every stem in App Paths and under every name in Program
Files, current first. Apps without former names see the same match and
the same candidates as before.

The impact is limited to early adopters: PdfCraft 0.4.0 and later
register as PdfCraft, and their MSI keeps the same UpgradeCode, so
installing from the launcher already upgraded a PrintCraft install in
place. What this fixes is the wrong status and the missing Uninstall
until then.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
2026-10-12 01:56:56 -04:00
a702ef16c8 Skip deb/rpm installs on rpm-ostree immutable systems (#107)
Fedora Atomic variants (Bazzite, Silverblue, etc.) block local dnf install;
prefer Flatpak and AppImage so one-click installs succeed without pkexec dnf.

Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 01:56:45 -04:00
0c3e639b4d Windows: compile DX12 shaders with FXC (#173)
wgpu's default DX12 compiler (Auto) loads whichever dxcompiler.dll the DLL
search path finds. The launcher ships none, and without a matching dxil.dll
the unsigned-DXIL warning fails wgpu's indirect-validation pipeline, the
device is lost and the launcher exits before the first frame. Pin the
compiler to FXC, which is part of Windows; WGPU_DX12_COMPILER still
overrides it. GL stays in the Windows backends: it is the deliberate
fallback for machines whose D3D12 adapters can't present to the window.

Fixes #126

Co-authored-by: Fabio Lussi <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 01:56:24 -04:00
Nezuko-Chan 41ded15851 Fallback apps to install from release tarballs when no usable Flatpak spawn is available (#87) 2026-10-12 01:54:08 -04:00
Gerknis 98c31aa066 Add experimental flake.nix for NixOS/Linux development (#72)
* Add experimental flake.nix for NixOS/Linux development

This adds an experimental flake-based development environment and package entry for ArtCraft Launcher.

What this adds:
- `flake.nix` at the repository root
- Rust toolchain via `rust-overlay`
- Required native Linux libraries for the Rust workspace
- A dev shell via `nix develop`
- A package/app target via `nix run` / `nix build`

Why:
ArtCraft Launcher is a Rust workspace and currently has no Nix flake support. This makes local development and installation much easier for NixOS/Linux users.

Notes:
- This is intentionally minimal and experimental.
- It targets the development environment and local package use.
- Additional refinements may be needed for specific Wayland/X11 setups.

* Fix flake metadata to match ArtCraft Launcher and remove GTK dependency

* add flake.lock

* Update flake.nix: replace deprecated xorg packages with their new names
2026-10-12 01:54:03 -04:00
Diego OspinaandDiego Ospina c064fde960 i18n: add Spanish (es) interface language (#196)
es.tsv translates the whole interface, the tray menu and tooltip, each
app's tagline and the backend's install/update messages, in neutral
international Spanish. Settings > Language offers Español; es, es-ES,
es-MX, es-419 and the like select it under Auto.

AI-assisted, native review welcome.

Models: Opus 5.5, Terra 5.6, DeepSeek V4 Flash

Co-authored-by: Diego Ospina <[email protected]>
2026-10-12 01:54:00 -04:00
96ed437966 Desktop: write log records to standard error and a rotating log file (#178)
A launcher started from the Dock, the Start menu or at login has no
terminal, and the Windows build has no console, so its log records and
panic reports were lost.

env_logger now writes each record to standard error and to
logs/artcraft-launcher.log in the launcher's data folder (portable mode
included). Each start keeps the previous two runs' logs; the file is
capped at 16 MiB. The file is opened once this is the only launcher
running, so a second start doesn't rotate the first one's log away.
Panics are reported through the logger. The default filter is now info
for the launcher's crates and warn for the rest; RUST_LOG still wins.

Ported from storytold/pdfcraft#239.

Co-authored-by: Fabio Lussi <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 01:53:54 -04:00
Osman Said e49bdc94dc Fix header label overlap in narrow windows (#130) 2026-10-12 01:53:50 -04:00
0358cda174 Linux: name the tray icon instead of showing the process id (#140)
tray-icon's default id is "<pid>-<n>" and the title is empty, so
StatusNotifierItem hosts showed numbers like "61965-1". On Linux and the
BSDs, give the icon the id "artcraft-launcher" and the title
"ArtCraft Launcher". Windows and macOS are unchanged.

Fixes #136

Co-authored-by: Fabio Lussi <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 01:53:46 -04:00
MarekSpodBiedry 7938ed90ac macOS: set the notification bundle id so no 'Where is use_default?' picker appears (#95) (#116) 2026-10-12 01:53:42 -04:00
Val EnfysandClaude Opus 5.5 ba8a704908 macOS: keep the bundle's icon in the Dock while the launcher runs (#127)
eframe sets the app icon at runtime, and on macOS that replaces the
Dock icon with a raw bitmap (the 256 px PNG we passed, or egui's logo
when none is given), which skips the system's icon treatment. Pass the
empty icon on macOS so eframe leaves the bundle's .icns alone.

Fixes #32.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 01:53:38 -04:00
Vikram Bhaskaran 1f92b7db17 Show real versions for PdfCraft and SoundCraft in the launcher. (#195)
Those binaries print a former id (`printcraft 0.2.1`) or the display
name (`SoundCraft 0.5.0`). Detection required an exact machine-id
prefix, so the version was missing and the card said "installed
installed · latest …". Parse --version against stems and the product
name, and stop using the word "installed" as a version placeholder.
2026-10-12 01:53:34 -04:00
XianZheYangandYangXZ2008 80f70a1a33 Keep feed repo renames inside storytold, and never install without a checksum (#208)
Two download-trust gaps from #90.

A repo rename in the release feed only had to look like owner/name, so
a compromised feed could send an app's downloads, or the launcher's own
self-update, to another account's repository with matching links and
checksums, and every check would vouch for them. A rename now has to
stay under `catalog::GITHUB_OWNER` (storytold) at both ends, or it is
dropped and the feed's entry stops matching, so GitHub decides as before.

A release with neither a per-file digest nor a SHA256SUMS.txt line was
still installed, checked by size only, with a note after the fact; a
same-length altered file would have passed. `install::download` now
refuses it before fetching anything, with an error that says why. With
a checksum always in hand, the size-only path goes: `http::download`
takes the SHA-256 as `&str`, and `Verified` and `keep_download` are
removed.

Tests: a rename within storytold is followed, one out of it is dropped
in either direction, and a release without a checksum fetches nothing
and never reaches the installer.

Fixes #90

Co-authored-by: YangXZ2008 <[email protected]>
2026-10-12 01:53:30 -04:00
MarekSpodBiedry 31a5e426e7 linux: only run pacman when the pacman database exists, so an unrelated pacman on PATH is not launched (#158) (#180) 2026-10-12 01:53:25 -04:00
XianZheYangandYangXZ2008 165f5358e0 Dialogs: keep the uninstall prompt at its own size, not the last dialog's (#207)
Every dialog shared one egui area id, "dialog". egui lays an area out in
the size it had the frame before, and the uninstall prompt's button row
was a right-to-left layout placed straight in the column, which fills
whatever height is left. So after Settings or the release notes had been
open, the prompt came up as tall as they were, with its buttons floating
in the middle, and never shrank back.

- Each dialog gets its own area id (`Dialog::key`), so a small prompt
  starts from its own size, and so does each feed alert.
- The uninstall prompt's and the feed alert's button rows sit in
  `ui.horizontal` like the other dialogs' rows, so they take only a
  row's height.
- A UI test opens Settings, closes it, opens the uninstall prompt and
  checks the buttons follow the text (they sat 243 px below it before).

Fixes #88

Co-authored-by: YangXZ2008 <[email protected]>
2026-10-12 01:53:22 -04:00
MarekSpodBiedryandskylar taylor-barrick 93caf01c4e fix(ui): remove third-party product comparisons
PR #84.

Closes #83.

Includes formatting-only corrections in the shared UI.

Co-authored-by: skylar taylor-barrick <[email protected]>
2026-10-11 23:38:46 -06:00
Dylan M. Taylor 7ba85e54f0 fix(ui): remove Classic and New app sections
PR #187.
2026-10-11 23:27:54 -06:00
Dylan M. Taylor 6124cf0693 fix(ui): enable website links for all Craft apps
PR #198.
2026-10-11 23:22:17 -06:00
Dylan M. Taylor 257e16c325 ci(actions): upgrade workflow actions to Node 24
Closes #159.
2026-10-11 22:44:40 -06:00
Brandon ThomasandClaude Opus 5.5 6e7a122866 Release 0.3.2: sign Windows builds again (#174)
Since the release workflow moved to craft-common-actions (#129), the caller
passed only the Apple and WINDOWS_CERTIFICATE secrets, not the AZURE_* ones that
Trusted Signing uses, so the 0.3.1 MSIs and portable exes came out unsigned.

Co-authored-by: Claude Opus 5.5 <[email protected]>
v0.3.2
2026-10-11 17:03:52 -04:00
Brandon ThomasandClaude Opus 5.5 b66486df82 Release 0.3.1: Linux riscv64 packages again under the shared release workflow (#170)
craft-common-actions calls `package.sh --formats deb,rpm,tar`, but package.sh
split formats on spaces only, so the riscv64 job built the binary, matched no
format, wrote nothing to dist/release and failed the 0.3.0 release at upload.
package.sh now accepts commas or spaces and rejects unknown format names.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-11 16:28:01 -04:00
Brandon ThomasandClaude Opus 5.5 41369d3416 Release 0.3.0; rustfmt platform.rs and install.rs (#160)
Bumps the workspace version to 0.3.0 for the new features since 0.2.1, and
applies cargo fmt to the two files from #149 that failed CI's format check.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-11 15:49:47 -04:00
Dylan M. Taylor c273a94a9a linux: fix Flatpak bundle updates, serialize installs, filter detection by arch, and add status logging (#149)
* linux: allow updating Flatpak bundles with --reinstall and improve error reporting

* linux: filter flatpak detection by arch, queue bundle installs, and log actions

* test(core): verify strict arch matching across all linux package formats
2026-10-11 14:49:33 -04:00
Dylan M. Taylor e2b01e7425 ci: refactor workflows to use storytold/craft-common-actions (#129)
* ci: refactor workflows to use reusable artcraft-common-actions

* ci: point reusable workflows to storytold/craft-common-actions

* ci: pin storytold/craft-common-actions to commit 27a978f and pass secrets explicitly

* ci: use @main (unpinned) for craft-common-actions with explanation comment
2026-10-11 11:51:50 -04:00
BFlatandClaude Opus 5.5 c1a9b36f82 macOS: open disk images reliably, and say why when they won't open; 0.2.1 (#135)
* macOS: open disk images reliably, and say why when they won't open

Installing or updating an app on macOS could fail with only "couldn't open
the disk image". hdiutil's own reason was thrown away, and every install of
an app mounted its image at the same folder, which an attach that was
interrupted or timed out could still be holding.

- Each install mounts its image at a new, empty folder.
- The attach skips hdiutil's checksum pass (-noverify): the download's
  SHA-256 is already checked against the release, and on a large image that
  pass alone could run past the time limit, now 5 minutes.
- When the image still won't open, or the app won't copy, the message says
  why in hdiutil's or ditto's words ("couldn't open the disk image:
  Resource temporarily unavailable").
- hdiutil, ditto and codesign are run by full path: a launcher started from
  Finder or at login gets a minimal PATH.

process::run is capture's sibling that returns the helper's error message;
capture is now built on it. The launcher's own macOS self-update installs
through the same code, so it gets all of this too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Release 0.2.1

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
v0.2.1
2026-10-11 19:51:54 +07:00
Brandon ThomasandClaude Opus 5.5 c5b073f5f7 UI tests: the title-bar test needs no GPU unless it saves a snapshot
It asked for a wgpu device up front, which panics where there is none: the
FreeBSD VM job has failed on it since it landed. Like the other tests, let
the default renderer create a device only when a snapshot is rendered.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 05:06:52 -04:00