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"),