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.
This commit is contained in:
Vikram Bhaskaran 2026-10-12 16:53:34 +11:00 committed by GitHub
parent 80f70a1a33
commit 1f92b7db17
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 79 additions and 16 deletions

View File

@ -86,6 +86,34 @@ pub(crate) fn appimage_version(name: &str, stems: &[&str]) -> Option<Option<Stri
(parsed.kind == artcraft_launcher_core::PackageKind::AppImage && parsed.token.starts_with("linux-")).then_some(Some(parsed.version))
}
/// Version from `<app> --version` output. Binaries print the machine id (`photocraft 0.7.0`),
/// the display name (`SoundCraft 0.5.0`), or a former id (`printcraft 0.2.1` for PdfCraft).
#[cfg_attr(not(any(target_os = "linux", target_os = "freebsd")), allow(dead_code))]
fn parse_cli_version(line: &str, app: &CraftApp) -> Option<version::Version> {
let line = line.trim();
for prefix in app.stems().chain(std::iter::once(app.name)) {
if let Some(rest) = strip_cli_prefix(line, prefix)
&& let Some(tok) = rest.split_whitespace().next()
&& let Some(v) = version::parse(tok)
{
return Some(v);
}
}
version::parse_package(line)
}
fn strip_cli_prefix<'a>(line: &'a str, prefix: &str) -> Option<&'a str> {
let (head, rest) = line.split_at_checked(prefix.len())?;
if !head.eq_ignore_ascii_case(prefix) {
return None;
}
match rest.chars().next() {
None => Some(rest),
Some(c) if c.is_ascii_whitespace() => Some(rest),
_ => None,
}
}
#[cfg(target_os = "macos")]
pub mod macos {
use std::path::{Path, PathBuf};
@ -339,7 +367,7 @@ pub mod linux {
/// to whatever installed it, since it can't tell which package manager that was.
fn detect_system_install(app: &CraftApp) -> Option<Installed> {
let bin = find_binary(app.id)?;
Some(Installed { version: binary_version(&bin, app.id), source: InstallSource::Binary, location: bin.display().to_string(), package: None })
Some(Installed { version: binary_version(&bin, app), source: InstallSource::Binary, location: bin.display().to_string(), package: None })
}
fn find_binary(id: &str) -> Option<PathBuf> {
@ -348,10 +376,9 @@ pub mod linux {
on_path.or_else(|| usual.map(|d| d.join(id)).find(|p| p.is_file()))
}
pub(crate) fn binary_version(bin: &Path, id: &str) -> Option<version::Version> {
pub(crate) fn binary_version(bin: &Path, app: &CraftApp) -> Option<version::Version> {
let out = crate::process::capture(bin.to_str()?, &["--version"], Duration::from_secs(5))?;
let rest = out.trim().strip_prefix(id)?.trim_start();
version::parse(rest.split_whitespace().next()?)
parse_cli_version(out.trim(), app)
}
}
@ -367,14 +394,13 @@ pub mod freebsd {
if !bin.exists() {
return None;
}
Some(Installed { version: binary_version(&bin, app.id), source: InstallSource::Binary, location: bin.display().to_string(), package: None })
Some(Installed { version: binary_version(&bin, app), source: InstallSource::Binary, location: bin.display().to_string(), package: None })
}
/// `<app> --version` prints `<app> 0.5.0 (sha, date)`.
fn binary_version(bin: &Path, id: &str) -> Option<version::Version> {
fn binary_version(bin: &Path, app: &CraftApp) -> Option<version::Version> {
let out = crate::process::capture(bin.to_str()?, &["--version"], Duration::from_secs(5))?;
let rest = out.trim().strip_prefix(id)?.trim_start();
version::parse(rest.split_whitespace().next()?)
parse_cli_version(out.trim(), app)
}
}
@ -412,9 +438,25 @@ mod linux_tests {
std::fs::write(&bin, "#!/bin/sh\necho 'photocraft 0.6.0 (0c72d9542, 2026-10-10)'\n").unwrap();
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&bin, std::fs::Permissions::from_mode(0o755)).unwrap();
let ver = linux::binary_version(&bin, photo.id);
let ver = linux::binary_version(&bin, photo);
assert_eq!(ver.unwrap().to_string(), "0.6.0");
}
#[test]
fn reads_binary_version_when_the_cli_uses_the_display_name_or_a_former_id() {
let temp_dir = tempfile::tempdir().unwrap();
use std::os::unix::fs::PermissionsExt;
let write = |name: &str, script: &str| {
let bin = temp_dir.path().join(name);
std::fs::write(&bin, script).unwrap();
std::fs::set_permissions(&bin, std::fs::Permissions::from_mode(0o755)).unwrap();
bin
};
let sound = write("soundcraft", "#!/bin/sh\necho 'SoundCraft 0.5.0'\n");
assert_eq!(linux::binary_version(&sound, CraftApp::by_id("soundcraft").unwrap()).unwrap().to_string(), "0.5.0");
let pdf = write("pdfcraft", "#!/bin/sh\necho 'printcraft 0.2.1'\n");
assert_eq!(linux::binary_version(&pdf, CraftApp::by_id("pdfcraft").unwrap()).unwrap().to_string(), "0.2.1");
}
}
#[cfg(test)]
@ -446,6 +488,18 @@ mod tests {
}
}
#[test]
fn parse_cli_version_accepts_id_display_name_and_former_id() {
let photo = CraftApp::by_id("photocraft").unwrap();
let sound = CraftApp::by_id("soundcraft").unwrap();
let pdf = CraftApp::by_id("pdfcraft").unwrap();
assert_eq!(parse_cli_version("photocraft 0.7.0 (dev build)", photo).unwrap().to_string(), "0.7.0");
assert_eq!(parse_cli_version("SoundCraft 0.5.0", sound).unwrap().to_string(), "0.5.0");
assert_eq!(parse_cli_version("printcraft 0.2.1", pdf).unwrap().to_string(), "0.2.1");
assert_eq!(parse_cli_version("PdfCraft 0.6.0", pdf).unwrap().to_string(), "0.6.0");
assert!(strip_cli_prefix("photocraft-cli 1.0.0", "photocraft").is_none());
}
#[cfg(target_os = "linux")]
#[test]
fn finds_appimages_in_every_folder_newest_first() {

View File

@ -203,6 +203,7 @@ month Dec Dez.
{version} installed → {latest} released {version} installiert → {latest} veröffentlicht
Version {version} Version {version}
{installed} installed · latest {latest} {installed} installiert · neueste {latest}
Installed · latest {latest} Installiert · neueste {latest}
Version {version} installed Version {version} installiert
Version {latest} Version {latest}
No release published yet Noch keine Version veröffentlicht

Can't render this file because it contains an unexpected character in line 11 and column 16.

View File

@ -201,6 +201,7 @@ month Dec déc.
{version} installed → {latest} released {version} installée → {latest} publiée
Version {version} Version {version}
{installed} installed · latest {latest} {installed} installée · dernière {latest}
Installed · latest {latest} Installée · dernière {latest}
Version {version} installed Version {version} installée
Version {latest} Version {latest}
No release published yet Aucune version publiée pour l'instant

Can't render this file because it contains an unexpected character in line 5 and column 50.

View File

@ -191,6 +191,7 @@ month Dec dez.
{version} installed → {latest} released {version} instalado → {latest} lançado
Version {version} Versão {version}
{installed} installed · latest {latest} {installed} instalado · última {latest}
Installed · latest {latest} Instalado · última {latest}
Version {version} installed Versão {version} instalada
Version {latest} Versão {latest}
No release published yet Nenhum lançamento publicado ainda

Can't render this file because it has a wrong number of fields in line 9.

View File

@ -201,6 +201,7 @@ month Dec дек.
{version} installed → {latest} released {version} установлено → {latest} выпущено
Version {version} Версия {version}
{installed} installed · latest {latest} {installed} установлено · последняя {latest}
Installed · latest {latest} Установлено · последняя {latest}
Version {version} installed Версия {version} установлена
Version {latest} Версия {latest}
No release published yet Релиз ещё не опубликован

Can't render this file because it contains an unexpected character in line 13 and column 44.

View File

@ -126,6 +126,7 @@ month Dec 12月
{version} installed → {latest} released 已安装 {version} → 最新 {latest}
Version {version} 版本 {version}
{installed} installed · latest {latest} 已安装 {installed} · 最新 {latest}
Installed · latest {latest} 已安装 · 最新 {latest}
Version {version} installed 已安装版本 {version}
Version {latest} 版本 {latest}
No release published yet 尚未发布版本

Can't render this file because it contains an unexpected character in line 4 and column 3.

View File

@ -57,7 +57,7 @@ impl AppEntry {
}
pub fn installed_label(&self) -> Option<String> {
self.installed.as_ref().map(|i| i.version.as_ref().map_or_else(|| t("installed").to_string(), ToString::to_string))
self.installed.as_ref().map(|i| i.version.as_ref().map(ToString::to_string).unwrap_or_else(|| t("Installed").to_string()))
}
pub fn latest_label(&self) -> Option<String> {

View File

@ -847,14 +847,18 @@ fn sort_label(k: SortKey) -> &'static str {
/// "0.4.1 installed → 0.5.0 released · Oct 8, 2026" and friends.
fn version_line(e: &AppEntry, status: &AppStatus) -> String {
let released = e.release().and_then(|r| r.published_at).map(|at| format!(" · {}", date_text(at))).unwrap_or_default();
match (status, e.installed_label(), e.latest_label()) {
let inst_ver = e.installed.as_ref().and_then(|i| i.version.as_ref().map(ToString::to_string));
let latest = e.latest_label();
match (status, inst_ver.as_deref(), latest.as_deref()) {
(AppStatus::UpdateAvailable, Some(i), Some(l)) => {
format!("{}{released}", fmt(tl!("{version} installed → {latest} released"), &[("version", i.as_str()), ("latest", l.as_str())]))
format!("{}{released}", fmt(tl!("{version} installed → {latest} released"), &[("version", i), ("latest", l)]))
}
(AppStatus::UpToDate, Some(i), _) => format!("{}{released}", fmt(tl!("Version {version}"), &[("version", i.as_str())])),
(_, Some(i), Some(l)) => fmt(tl!("{installed} installed · latest {latest}"), &[("installed", i.as_str()), ("latest", l.as_str())]),
(_, Some(i), None) => fmt(tl!("Version {version} installed"), &[("version", i.as_str())]),
(_, None, Some(l)) => format!("{}{released}", fmt(tl!("Version {latest}"), &[("latest", l.as_str())])),
(AppStatus::UpToDate, Some(i), _) => format!("{}{released}", fmt(tl!("Version {version}"), &[("version", i)])),
(_, Some(i), Some(l)) => fmt(tl!("{installed} installed · latest {latest}"), &[("installed", i), ("latest", l)]),
(_, None, Some(l)) if e.installed.is_some() => fmt(tl!("Installed · latest {latest}"), &[("latest", l)]),
(_, Some(i), None) => fmt(tl!("Version {version} installed"), &[("version", i)]),
(_, None, None) if e.installed.is_some() => tl!("Installed").to_string(),
(_, None, Some(l)) => format!("{}{released}", fmt(tl!("Version {latest}"), &[("latest", l)])),
(AppStatus::NoRelease, None, None) => tl!("No release published yet").to_string(),
_ => " ".to_string(),
}