diff --git a/README.md b/README.md
index 2b1d5cc9..6a1b74bf 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@
One place to install, update and open every app in the Craft App Suite, rebuilt in pure Rust.
- PhotoCraft, LightCraft, FilmCraft, DesignCraft, VectorCraft, PdfCraft, EffectCraft, WordCraft,
+ ArtCraft Studio, PhotoCraft, LightCraft, FilmCraft, DesignCraft, VectorCraft, PdfCraft, EffectCraft, WordCraft,
DeckCraft, GridCraft, CADCraft, SoundCraft, FigCraft, Craft Anything, TextureCraft, MaterialCraft and the community app SolidCraft: the right build for your computer, kept up to date.
@@ -264,27 +264,26 @@ ArtCraft Launcher installs the **Craft App Suite**: free, open-source creative t
[ArtCraft](https://getartcraft.com/) team, each written from scratch in Rust and each able to
stand on its own.
-| | App | What it's for | Code | Learn more |
-|:-:|---|---|---|---|
-|
| **PhotoCraft** | Image editing: layers, masks, type and real PSD files | [GitHub](https://github.com/storytold/photocraft) | [Website](https://getartcraft.com/apps/photocraft) |
-|
| **VectorCraft** | Vector illustration | [GitHub](https://github.com/storytold/vectorcraft) | [Website](https://getartcraft.com/apps/vectorcraft) |
-|
| **FilmCraft** | Video editing, color and sound | [GitHub](https://github.com/storytold/filmcraft) | [Website](https://getartcraft.com/apps/filmcraft) |
-|
| **LightCraft** | Photo library and raw development | [GitHub](https://github.com/storytold/lightcraft) | [Website](https://getartcraft.com/apps/lightcraft) |
-|
| **PdfCraft** | Reading, organizing and protecting PDFs | [GitHub](https://github.com/storytold/pdfcraft) | [Website](https://getartcraft.com/apps/pdfcraft) |
-|
| **EffectCraft** | Motion graphics and visual effects | [GitHub](https://github.com/storytold/effectcraft) | [Website](https://getartcraft.com/apps/effectcraft) |
-|
| **DesignCraft** | Page layout and publishing | [GitHub](https://github.com/storytold/designcraft) | [Website](https://getartcraft.com/apps/designcraft) |
-|
| **FigCraft** | Interface design and prototyping | [GitHub](https://github.com/storytold/figcraft) | [Website](https://getartcraft.com/apps/figcraft) |
-|
| **WordCraft** | Word processing and documents | [GitHub](https://github.com/storytold/wordcraft) | [Website](https://getartcraft.com/apps/wordcraft) |
-|
| **SoundCraft** | Audio recording and editing | [GitHub](https://github.com/storytold/soundcraft) | [Website](https://getartcraft.com/apps/soundcraft) |
-|
| **DeckCraft** | Presentations and slide decks | [GitHub](https://github.com/storytold/deckcraft) | [Website](https://getartcraft.com/apps/deckcraft) |
-|
| **CADCraft** | Computer-aided design | [GitHub](https://github.com/storytold/cadcraft) | [Website](https://getartcraft.com/apps/cadcraft) |
-|
| **GridCraft** | Spreadsheets and data | [GitHub](https://github.com/storytold/gridcraft) | [Website](https://getartcraft.com/apps/gridcraft) |
-|
| **TextureCraft** | 3D texture painting | [GitHub](https://github.com/storytold/texturecraft) | [Website](https://getartcraft.com/apps/texturecraft) |
-|
| **MaterialCraft** | Procedural material design | [GitHub](https://github.com/storytold/materialcraft) | [Project page](https://github.com/storytold/materialcraft) |
-|
| **Craft Anything** | See how any software works, then build your own | [GitHub](https://github.com/storytold/craft-anything) | [Website](https://getartcraft.com/apps/craft-anything) |
-|
| **SolidCraft** | Parametric 3D CAD by Tangency Labs (community app) | [GitHub](https://github.com/tangency-labs/solidcraft) | |
-
-And [**ArtCraft**](https://getartcraft.com/) itself, our AI image and video studio for artists who want real control.
+| | App | What it's for | Code | Learn more |
+|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:--------------------|:------------------------------------------------------|:------------------------------------------------------|:-----------------------------------------------------------|
+|
| **ArtCraft Studio** | AI image and video creation | [GitHub](https://github.com/storytold/artcraft) | [Website](https://getartcraft.com) |
+|
| **PhotoCraft** | Image editing: layers, masks, type and real PSD files | [GitHub](https://github.com/storytold/photocraft) | [Website](https://getartcraft.com/apps/photocraft) |
+|
| **VectorCraft** | Vector illustration | [GitHub](https://github.com/storytold/vectorcraft) | [Website](https://getartcraft.com/apps/vectorcraft) |
+|
| **FilmCraft** | Video editing, color and sound | [GitHub](https://github.com/storytold/filmcraft) | [Website](https://getartcraft.com/apps/filmcraft) |
+|
| **LightCraft** | Photo library and raw development | [GitHub](https://github.com/storytold/lightcraft) | [Website](https://getartcraft.com/apps/lightcraft) |
+|
| **PdfCraft** | Reading, organizing and protecting PDFs | [GitHub](https://github.com/storytold/pdfcraft) | [Website](https://getartcraft.com/apps/pdfcraft) |
+|
| **EffectCraft** | Motion graphics and visual effects | [GitHub](https://github.com/storytold/effectcraft) | [Website](https://getartcraft.com/apps/effectcraft) |
+|
| **DesignCraft** | Page layout and publishing | [GitHub](https://github.com/storytold/designcraft) | [Website](https://getartcraft.com/apps/designcraft) |
+|
| **FigCraft** | Interface design and prototyping | [GitHub](https://github.com/storytold/figcraft) | [Website](https://getartcraft.com/apps/figcraft) |
+|
| **WordCraft** | Word processing and documents | [GitHub](https://github.com/storytold/wordcraft) | [Website](https://getartcraft.com/apps/wordcraft) |
+|
| **SoundCraft** | Audio recording and editing | [GitHub](https://github.com/storytold/soundcraft) | [Website](https://getartcraft.com/apps/soundcraft) |
+|
| **DeckCraft** | Presentations and slide decks | [GitHub](https://github.com/storytold/deckcraft) | [Website](https://getartcraft.com/apps/deckcraft) |
+|
| **CADCraft** | Computer-aided design | [GitHub](https://github.com/storytold/cadcraft) | [Website](https://getartcraft.com/apps/cadcraft) |
+|
| **GridCraft** | Spreadsheets and data | [GitHub](https://github.com/storytold/gridcraft) | [Website](https://getartcraft.com/apps/gridcraft) |
+|
| **TextureCraft** | 3D texture painting | [GitHub](https://github.com/storytold/texturecraft) | [Website](https://getartcraft.com/apps/texturecraft) |
+|
| **MaterialCraft** | Procedural material design | [GitHub](https://github.com/storytold/materialcraft) | [Project page](https://github.com/storytold/materialcraft) |
+|
| **Craft Anything** | See how any software works, then build your own | [GitHub](https://github.com/storytold/craft-anything) | [Website](https://getartcraft.com/apps/craft-anything) |
+|
| **SolidCraft** | Parametric 3D CAD by Tangency Labs (community app) | [GitHub](https://github.com/tangency-labs/solidcraft) | |
diff --git a/crates/core/src/catalog.rs b/crates/core/src/catalog.rs
index 737ede9b..487538e4 100644
--- a/crates/core/src/catalog.rs
+++ b/crates/core/src/catalog.rs
@@ -75,6 +75,14 @@ impl CraftApp {
kind != crate::PackageKind::Dmg || self.mac_team.is_some()
}
+ /// Whether this app runs on the given operating system.
+ pub fn runs_on(&self, os: crate::Os) -> bool {
+ match os {
+ crate::Os::FreeBsd => self.id != "artcraft",
+ _ => true,
+ }
+ }
+
/// Bundle ids this app may be installed under, current first.
pub fn bundle_ids(&self) -> impl Iterator- + '_ {
std::iter::once(self.bundle_id()).chain(self.former_bundles.iter().map(|b| b.to_string()))
@@ -398,4 +406,16 @@ mod tests {
assert_eq!(pdf.website(), "https://getartcraft.com/apps/pdfcraft");
assert_eq!(CraftApp::by_id("wordcraft").unwrap().website(), "https://getartcraft.com/apps/wordcraft");
}
+
+ #[test]
+ fn artcraft_studio_does_not_run_on_freebsd() {
+ let studio = CraftApp::by_id("artcraft").unwrap();
+ assert!(!studio.runs_on(crate::Os::FreeBsd));
+ assert!(studio.runs_on(crate::Os::Linux));
+ assert!(studio.runs_on(crate::Os::MacOs));
+ assert!(studio.runs_on(crate::Os::Windows));
+
+ let photo = CraftApp::by_id("photocraft").unwrap();
+ assert!(photo.runs_on(crate::Os::FreeBsd));
+ }
}
diff --git a/crates/core/src/platform.rs b/crates/core/src/platform.rs
index bc34c2f5..a4968653 100644
--- a/crates/core/src/platform.rs
+++ b/crates/core/src/platform.rs
@@ -205,8 +205,16 @@ impl AssetName {
}
}
-/// Reads `---[-portable].`; `None` for checksums, zsync files, CLI
-/// and web bundles, or anything else.
+fn strip_stem_prefix<'a>(base: &'a str, stem: &str, sep: char) -> Option<&'a str> {
+ if base.len() > stem.len() && base[..stem.len()].eq_ignore_ascii_case(stem) && base.as_bytes()[stem.len()] == sep as u8 {
+ Some(&base[stem.len() + 1..])
+ } else {
+ None
+ }
+}
+
+/// Reads `---[-portable].` or Studio's `__[_].`;
+/// `None` for checksums, zsync files, CLI and web bundles, or anything else.
pub fn parse_asset_name(name: &str, stems: &[&str]) -> Option {
const KINDS: [PackageKind; 9] = [
PackageKind::PortableZip,
@@ -221,14 +229,35 @@ pub fn parse_asset_name(name: &str, stems: &[&str]) -> Option {
];
let (kind, base) = KINDS.iter().find_map(|k| name.strip_suffix(k.suffix()).map(|b| (*k, b)))?;
for stem in stems {
- let Some(rest) = base.strip_prefix(stem).and_then(|r| r.strip_prefix('-')) else { continue };
- // rest = --; os and arch never contain '-', versions may.
- let mut parts = rest.rsplitn(3, '-');
- let (arch, os, version) = (parts.next()?, parts.next()?, parts.next()?);
- if crate::version::parse(version).is_none() || !version.starts_with(|c: char| c.is_ascii_digit()) {
- continue;
+ if let Some(rest) = strip_stem_prefix(base, stem, '-') {
+ // rest = --; os and arch never contain '-', versions may.
+ let mut parts = rest.rsplitn(3, '-');
+ let (Some(arch), Some(os), Some(version)) = (parts.next(), parts.next(), parts.next()) else { continue };
+ if crate::version::parse(version).is_none() || !version.starts_with(|c: char| c.is_ascii_digit()) {
+ continue;
+ }
+ return Some(AssetName { stem: (*stem).to_string(), version: version.to_string(), token: format!("{os}-{arch}"), kind });
+ }
+ if let Some(rest) = strip_stem_prefix(base, stem, '_') {
+ // Studio / Tauri format: _[_]
+ let mut parts = rest.split('_');
+ let (Some(version), Some(arch_token)) = (parts.next(), parts.next()) else { continue };
+ if crate::version::parse(version).is_none() || !version.starts_with(|c: char| c.is_ascii_digit()) {
+ continue;
+ }
+ let token = match (kind, arch_token) {
+ (PackageKind::Dmg, "universal") => Some("macos-universal".to_string()),
+ (PackageKind::Dmg, "x86_64" | "x64") => Some("macos-x86_64".to_string()),
+ (PackageKind::Dmg, "aarch64" | "arm64") => Some("macos-aarch64".to_string()),
+ (PackageKind::Msi | PackageKind::PortableZip, "x86_64" | "x64") => Some("windows-x64".to_string()),
+ (PackageKind::Msi | PackageKind::PortableZip, "x86") => Some("windows-x86".to_string()),
+ (PackageKind::Msi | PackageKind::PortableZip, "arm64" | "aarch64") => Some("windows-arm64".to_string()),
+ _ => None,
+ };
+ if let Some(token) = token {
+ return Some(AssetName { stem: (*stem).to_string(), version: version.to_string(), token, kind });
+ }
}
- return Some(AssetName { stem: (*stem).to_string(), version: version.to_string(), token: format!("{os}-{arch}"), kind });
}
None
}
@@ -304,6 +333,27 @@ mod tests {
}
}
+ #[test]
+ fn artcraft_studio_selects_published_desktop_assets() {
+ let release =
+ crate::release::parse_github_release(include_str!("../tests/fixtures/artcraft-release.json"), "storytold/artcraft").unwrap();
+ assert_eq!(release.version, Some(crate::version::Version::new(0, 41, 1)));
+ for (os, arch, kind, expected) in [
+ (Os::Windows, Arch::X64, PackageKind::Msi, "ArtCraft_0.41.1_x64_en-US.msi"),
+ (Os::MacOs, Arch::Arm64, PackageKind::Dmg, "ArtCraft_0.41.1_universal.dmg"),
+ (Os::MacOs, Arch::X64, PackageKind::Dmg, "ArtCraft_0.41.1_universal.dmg"),
+ (Os::Linux, Arch::X64, PackageKind::AppImage, "artcraft-0.41.1-linux-x86_64.AppImage"),
+ (Os::Linux, Arch::X64, PackageKind::Flatpak, "artcraft-0.41.1-linux-x86_64.flatpak"),
+ (Os::Linux, Arch::X64, PackageKind::TarGz, "artcraft-0.41.1-linux-x86_64.tar.gz"),
+ (Os::Linux, Arch::Arm64, PackageKind::TarGz, "artcraft-0.41.1-linux-aarch64.tar.gz"),
+ ] {
+ let platform = Platform { os, arch };
+ let (asset, selected) = pick_asset(&release, &["artcraft"], platform, &[kind]).unwrap();
+ assert_eq!(asset.name, expected);
+ assert_eq!(selected, kind);
+ }
+ }
+
#[test]
fn freebsd_picks_the_tarball_for_the_processor() {
// SoundCraft v0.10.0's GitHub metadata, trimmed to its FreeBSD tarballs.
@@ -382,6 +432,15 @@ mod tests {
assert!(parse_asset_name("figcraft-0.5.1-linux-x86_64.tar.gz", &["figcraft"]).is_some());
}
+ #[test]
+ fn reads_artcraft_studio_asset_names() {
+ let dmg = parse_asset_name("ArtCraft_0.41.1_universal.dmg", &["artcraft"]).unwrap();
+ assert_eq!((dmg.version.as_str(), dmg.token.as_str(), dmg.kind), ("0.41.1", "macos-universal", PackageKind::Dmg));
+
+ let msi = parse_asset_name("ArtCraft_0.41.1_x64_en-US.msi", &["artcraft"]).unwrap();
+ assert_eq!((msi.version.as_str(), msi.token.as_str(), msi.kind), ("0.41.1", "windows-x64", PackageKind::Msi));
+ }
+
#[test]
fn picks_the_native_build_first_and_falls_back_to_emulation() {
let r = release(PHOTO);
diff --git a/crates/core/tests/fixtures/artcraft-release.json b/crates/core/tests/fixtures/artcraft-release.json
new file mode 100644
index 00000000..bbabc603
--- /dev/null
+++ b/crates/core/tests/fixtures/artcraft-release.json
@@ -0,0 +1,76 @@
+{
+ "tag_name": "artcraft-v0.41.1",
+ "name": "ArtCraft v0.41.1",
+ "html_url": "https://github.com/storytold/artcraft/releases/tag/artcraft-v0.41.1",
+ "published_at": "2026-11-07T02:49:42Z",
+ "prerelease": false,
+ "draft": false,
+ "assets": [
+ {
+ "name": "artcraft-0.41.1-linux-aarch64.AppImage",
+ "size": 177187336,
+ "browser_download_url": "https://github.com/storytold/artcraft/releases/download/artcraft-v0.41.1/artcraft-0.41.1-linux-aarch64.AppImage",
+ "digest": "sha256:31a4071758ce7989c6548e3976839aaf6010be414d0a1b5cc9a913537bbc88de"
+ },
+ {
+ "name": "artcraft-0.41.1-linux-aarch64.flatpak",
+ "size": 40561288,
+ "browser_download_url": "https://github.com/storytold/artcraft/releases/download/artcraft-v0.41.1/artcraft-0.41.1-linux-aarch64.flatpak",
+ "digest": "sha256:8f899c94f0c41fb1553c66fc6b4d3ac0763f4c034bfb06fe9f0d60426c153f7b"
+ },
+ {
+ "name": "artcraft-0.41.1-linux-aarch64.tar.gz",
+ "size": 49710171,
+ "browser_download_url": "https://github.com/storytold/artcraft/releases/download/artcraft-v0.41.1/artcraft-0.41.1-linux-aarch64.tar.gz",
+ "digest": "sha256:cb780e02e87234927d339c87dee03b224e1641320758be813d31d442d9a691ae"
+ },
+ {
+ "name": "artcraft-0.41.1-linux-riscv64.tar.gz",
+ "size": 48491446,
+ "browser_download_url": "https://github.com/storytold/artcraft/releases/download/artcraft-v0.41.1/artcraft-0.41.1-linux-riscv64.tar.gz",
+ "digest": "sha256:d32edc5ac3320f6789154fcd89552f0650d6d396c8cb576d1197d1e11bbc0745"
+ },
+ {
+ "name": "artcraft-0.41.1-linux-x86_64.AppImage",
+ "size": 182495736,
+ "browser_download_url": "https://github.com/storytold/artcraft/releases/download/artcraft-v0.41.1/artcraft-0.41.1-linux-x86_64.AppImage",
+ "digest": "sha256:bb32657b6e81e8e47da44ca4e764b1af14d77d8690b248a263a4acd1218b7d2d"
+ },
+ {
+ "name": "artcraft-0.41.1-linux-x86_64.flatpak",
+ "size": 41755576,
+ "browser_download_url": "https://github.com/storytold/artcraft/releases/download/artcraft-v0.41.1/artcraft-0.41.1-linux-x86_64.flatpak",
+ "digest": "sha256:7351e1bad48006b251ab943372fa15f1829d8b92ca944adb7f312b4e5c7e9912"
+ },
+ {
+ "name": "artcraft-0.41.1-linux-x86_64.tar.gz",
+ "size": 49365185,
+ "browser_download_url": "https://github.com/storytold/artcraft/releases/download/artcraft-v0.41.1/artcraft-0.41.1-linux-x86_64.tar.gz",
+ "digest": "sha256:149f9748ea33553177c021e1ade47a195fad25b881129d4178fe5bd922a94eb4"
+ },
+ {
+ "name": "ArtCraft_0.41.1_universal.dmg",
+ "size": 94884865,
+ "browser_download_url": "https://github.com/storytold/artcraft/releases/download/artcraft-v0.41.1/ArtCraft_0.41.1_universal.dmg",
+ "digest": "sha256:89009b8983d88919481ac936e2ada0409dff0e0fc73f487cce3e7324699866a6"
+ },
+ {
+ "name": "ArtCraft_0.41.1_x64_en-US.msi",
+ "size": 47058944,
+ "browser_download_url": "https://github.com/storytold/artcraft/releases/download/artcraft-v0.41.1/ArtCraft_0.41.1_x64_en-US.msi",
+ "digest": "sha256:450b15e53d8ccc9afde2d3644a547ab5f900167b837b861571ea4c833166417d"
+ },
+ {
+ "name": "ArtCraft_universal.app.tar.gz",
+ "size": 94791060,
+ "browser_download_url": "https://github.com/storytold/artcraft/releases/download/artcraft-v0.41.1/ArtCraft_universal.app.tar.gz",
+ "digest": "sha256:d1e4e9623ebf506fbc8d7476b69b2db957125761e7908624b3eb031ce044413e"
+ },
+ {
+ "name": "SHA256SUMS.txt",
+ "size": 4108,
+ "browser_download_url": "https://github.com/storytold/artcraft/releases/download/artcraft-v0.41.1/SHA256SUMS.txt",
+ "digest": "sha256:5450801c2342f8ea05d9aa71cd31cb4b02462cebd9cab7abbe8884e17de6f075"
+ }
+ ]
+}
diff --git a/crates/native/src/backend.rs b/crates/native/src/backend.rs
index bedf5ddf..74a21d87 100644
--- a/crates/native/src/backend.rs
+++ b/crates/native/src/backend.rs
@@ -246,7 +246,7 @@ impl NativeBackend {
let pre = options.prereleases;
let feed = if pre { None } else { load_feed(&self.paths) };
let now = crate::now();
- for app in &SUITE {
+ for app in SUITE.iter().filter(|a| self.platform.is_none_or(|p| a.runs_on(p.os))) {
let answer = match feed.as_ref().map(|f| f.answer(app, now)) {
Some(FeedAnswer::Fresh(Some(r))) => Some((LatestRelease::Found(r), Origin::ArtCraftApi)),
Some(FeedAnswer::Fresh(None)) => Some((LatestRelease::None, Origin::ArtCraftApi)),
@@ -268,7 +268,7 @@ impl NativeBackend {
let paths = self.paths.clone();
let platform = self.platform;
std::thread::spawn(move || {
- for app in &SUITE {
+ for app in SUITE.iter().filter(|a| platform.is_none_or(|p| a.runs_on(p.os))) {
report_partial(app, &shared, &paths, platform);
}
});
@@ -293,12 +293,12 @@ impl NativeBackend {
http::clean_downloads(&paths.downloads(), Duration::from_secs(30 * 86_400), Duration::from_secs(7 * 86_400));
let snapshot = crate::detect::Snapshot::take();
let manifest = shared.manifest.lock().unwrap_or_else(PoisonError::into_inner).clone();
- for app in &SUITE {
+ for app in SUITE.iter().filter(|a| platform.is_none_or(|p| a.runs_on(p.os))) {
let found = crate::detect::detect(app, &manifest, &snapshot);
publish_detected(app, found, &shared, platform);
}
// Downloads paused (or cut off by quitting) in an earlier run.
- for app in &SUITE {
+ for app in SUITE.iter().filter(|a| platform.is_none_or(|p| a.runs_on(p.os))) {
report_partial(app, &shared, &paths, platform);
}
drop(busy);
@@ -310,7 +310,7 @@ impl NativeBackend {
/// day old, or no answer at all), at most once an hour per app unless `force`.
fn check(&self, force: bool) {
let (Some(source), Some(http)) = (self.source.clone(), self.http.clone()) else {
- for app in &SUITE {
+ for app in SUITE.iter().filter(|a| self.platform.is_none_or(|p| a.runs_on(p.os))) {
self.shared.emit(Event::Latest {
app: app.id.to_string(),
latest: LatestRelease::Failed("secure connections aren't available".into()),
@@ -339,17 +339,23 @@ impl NativeBackend {
shared.emit(Event::Notices { announcements: f.announcements.clone(), alerts: f.alerts.clone() });
}
let now = crate::now();
- let queue = Mutex::new(SUITE.iter().chain(std::iter::once(&LAUNCHER)).collect::>());
+ let queue = Mutex::new(
+ SUITE
+ .iter()
+ .filter(|a| platform.is_none_or(|p| a.runs_on(p.os)))
+ .chain(std::iter::once(&LAUNCHER))
+ .collect::>(),
+ );
let sources = sweep(&queue, &mirror, feed.as_ref(), fallback, &source, &shared, &paths, (platform, &options), force, now);
source.save();
{
let answered = shared.answered.lock().unwrap_or_else(PoisonError::into_inner);
- if SUITE.iter().all(|a| answered.contains(a.id)) {
+ if SUITE.iter().filter(|a| platform.is_none_or(|p| a.runs_on(p.os))).all(|a| answered.contains(a.id)) {
shared.loaded.store(true, Ordering::SeqCst);
}
}
// A newer release changes which file an install would use.
- for app in &SUITE {
+ for app in SUITE.iter().filter(|a| platform.is_none_or(|p| a.runs_on(p.os))) {
report_partial(app, &shared, &paths, platform);
}
let source_used = source_used_of(&sources);
@@ -364,6 +370,9 @@ impl NativeBackend {
fn start_job(&self, app_id: &str, work: Work) {
let uninstall = matches!(work, Work::Uninstall);
let Some(app) = CraftApp::by_id(app_id) else { return };
+ if self.platform.is_some_and(|p| !app.runs_on(p.os)) {
+ return;
+ }
log::info!("{}: starting {} job", app.name, if uninstall { "uninstall" } else { "install/update" });
let stop = Arc::new(Stop::default());
let restarting = {
diff --git a/crates/ui-egui/src/app.rs b/crates/ui-egui/src/app.rs
index 9b6c258b..15d8bb4f 100644
--- a/crates/ui-egui/src/app.rs
+++ b/crates/ui-egui/src/app.rs
@@ -260,7 +260,7 @@ impl LauncherUi {
Self {
backend,
settings,
- entries: SUITE.iter().map(AppEntry::new).collect(),
+ entries: SUITE.iter().filter(|a| platform.is_none_or(|p| a.runs_on(p.os))).map(AppEntry::new).collect(),
caps,
platform,
checking: false,
@@ -926,6 +926,7 @@ impl LauncherUi {
.filter(|(_, e)| {
self.in_filter(e, self.settings.filter)
&& !(self.settings.hide_community_apps && e.app.kind == artcraft_launcher_core::catalog::AppKind::Community)
+ && self.platform.is_none_or(|p| e.app.runs_on(p.os))
&& (needle.is_empty() || e.app.name.to_lowercase().contains(&needle) || e.app.tagline.to_lowercase().contains(&needle))
})
.collect();
diff --git a/crates/ui-egui/src/details.rs b/crates/ui-egui/src/details.rs
index 5ee4c115..b53c5e4f 100644
--- a/crates/ui-egui/src/details.rs
+++ b/crates/ui-egui/src/details.rs
@@ -247,7 +247,12 @@ impl LauncherUi {
fn neighbour_apps(&self, id: &str) -> (Option, Option) {
let mut ids: Vec<&str> = self.visible_ids().into_iter().filter(|i| AppDetails::by_id(i).is_some()).collect();
if !ids.contains(&id) {
- ids = artcraft_launcher_core::SUITE.iter().map(|a| a.id).filter(|i| AppDetails::by_id(i).is_some()).collect();
+ ids = artcraft_launcher_core::SUITE
+ .iter()
+ .filter(|a| self.platform.is_none_or(|p| a.runs_on(p.os)))
+ .map(|a| a.id)
+ .filter(|i| AppDetails::by_id(i).is_some())
+ .collect();
}
let Some(at) = ids.iter().position(|i| *i == id) else { return (None, None) };
if ids.len() < 2 {
diff --git a/crates/ui-egui/tests/ui.rs b/crates/ui-egui/tests/ui.rs
index 24057a32..c1e3d9e1 100644
--- a/crates/ui-egui/tests/ui.rs
+++ b/crates/ui-egui/tests/ui.rs
@@ -788,6 +788,25 @@ fn a_freebsd_version_row_sizes_the_tarball() {
assert!(facts.ends_with(", 2026 ยท 140 MB"), "{facts:?}");
}
+#[test]
+fn freebsd_skips_artcraft_studio() {
+ let freebsd = Platform { os: Os::FreeBsd, arch: Arch::X64 };
+ let (h, _) = harness_full(
+ events(),
+ ViewMode::Grid,
+ egui::Theme::Dark,
+ true,
+ 1.0,
+ artcraft_launcher_ui_egui::TitleBar::Native,
+ Rc::default(),
+ Some(freebsd),
+ None,
+ );
+ let ids = h.state().as_ref().unwrap().visible_ids_for_test();
+ assert!(!ids.contains(&"artcraft"), "ArtCraft Studio must be skipped on FreeBSD: {ids:?}");
+ assert!(ids.contains(&"photocraft"), "PhotoCraft remains visible on FreeBSD");
+}
+
/// The launcher's own list in Settings sizes the launcher's download for this machine.
#[test]
fn the_launcher_version_list_sizes_the_launchers_download() {
diff --git a/xtask/src/local.rs b/xtask/src/local.rs
index 18ead311..22d2d582 100644
--- a/xtask/src/local.rs
+++ b/xtask/src/local.rs
@@ -19,6 +19,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
/// Every Craft App: (id = folder name, display name). Keep in step with `catalog::SUITE`.
const APPS: &[(&str, &str)] = &[
+ ("artcraft", "ArtCraft Studio"),
("photocraft", "PhotoCraft"),
("vectorcraft", "VectorCraft"),
("filmcraft", "FilmCraft"),