Fix #5236: find PhotoCraft where its Windows installer puts it

Per CodeRabbit on #5327: on Windows Edit Original looked for photocraft.exe
only on the PATH, but PhotoCraft's MSI installs it to ArtCraft\PhotoCraft
under %LOCALAPPDATA%\Programs (per user, the default) or Program Files (for
everyone), neither on the PATH. Look first at the path the installer
registers under App Paths (HKCU, then HKLM; `reg query`, as i18n reads the
locale, which also covers a folder chosen in setup), then those two default
folders, then the PATH. Each candidate must still be an .exe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ruchirguitar
2026-11-03 19:57:04 +07:00
committed by bflatastic
co-authored by Claude Opus 5.5
parent d7aa1c561f
commit 46f1ddf170
2 changed files with 108 additions and 6 deletions
+105 -4
View File
@@ -400,8 +400,8 @@ pub(crate) fn find_photocraft(home: Option<&std::path::Path>, path_var: Option<&
photocraft_candidates(home, path_var).into_iter().find(|p| check_app(p).is_ok())
}
/// Where PhotoCraft may be installed: the Applications folders on macOS, else a `photocraft`
/// program on the `PATH`.
/// Where PhotoCraft may be installed: the Applications folders on macOS; on Windows where its
/// installer puts it ([`windows_candidates`]); else a `photocraft` program on the `PATH`.
pub(crate) fn photocraft_candidates(home: Option<&std::path::Path>, path_var: Option<&std::ffi::OsStr>) -> Vec<std::path::PathBuf> {
if cfg!(target_os = "macos") {
let mut v = vec![std::path::PathBuf::from("/Applications/PhotoCraft.app")];
@@ -409,7 +409,73 @@ pub(crate) fn photocraft_candidates(home: Option<&std::path::Path>, path_var: Op
return v;
}
let name = if cfg!(windows) { "photocraft.exe" } else { "photocraft" };
path_var.map(|p| std::env::split_paths(p).map(|dir| dir.join(name)).collect()).unwrap_or_default()
let on_path = path_var.map(|p| std::env::split_paths(p).map(|dir| dir.join(name)).collect()).unwrap_or_default();
if cfg!(windows) {
let folders: Vec<std::path::PathBuf> = [
std::env::var_os("LOCALAPPDATA").map(|d| std::path::PathBuf::from(d).join("Programs")),
std::env::var_os("ProgramW6432").map(std::path::PathBuf::from),
std::env::var_os("ProgramFiles").map(std::path::PathBuf::from),
]
.into_iter()
.flatten()
.collect();
return windows_candidates(registered_photocraft(), &folders, on_path);
}
on_path
}
/// Windows: PhotoCraft's installer puts it in `ArtCraft\PhotoCraft` under a folder of
/// `program_folders` (for the current user `%LOCALAPPDATA%\Programs`, for everyone Program Files)
/// and registers its path under App Paths, which also covers a folder chosen in setup. So: the
/// `registered` paths first, then those folders, then `on_path`.
pub(crate) fn windows_candidates(
registered: Vec<std::path::PathBuf>,
program_folders: &[std::path::PathBuf],
on_path: Vec<std::path::PathBuf>,
) -> Vec<std::path::PathBuf> {
let mut v = registered;
for f in program_folders {
let p = f.join("ArtCraft").join("PhotoCraft").join("photocraft.exe");
if !v.contains(&p) {
v.push(p);
}
}
v.extend(on_path);
v
}
/// The paths PhotoCraft's installer registers under App Paths, for the current user and then for
/// everyone (`reg query`, so no extra dependency; a hidden console window).
#[cfg(windows)]
fn registered_photocraft() -> Vec<std::path::PathBuf> {
use std::os::windows::process::CommandExt as _;
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
// reg.exe by its full path, so nothing named `reg` in the app's folder or on PATH runs instead.
let root = std::env::var_os("SystemRoot").map(std::path::PathBuf::from).unwrap_or_else(|| r"C:\Windows".into());
["HKCU", "HKLM"]
.into_iter()
.filter_map(|hive| {
let key = format!(r"{hive}\Software\Microsoft\Windows\CurrentVersion\App Paths\photocraft.exe");
let out = std::process::Command::new(root.join("System32").join("reg.exe"))
.args(["query", &key, "/ve"])
.creation_flags(CREATE_NO_WINDOW)
.output()
.ok()?;
registered_path(&String::from_utf8_lossy(&out.stdout))
})
.collect()
}
#[cfg(not(windows))]
fn registered_photocraft() -> Vec<std::path::PathBuf> {
Vec::new()
}
/// The path in `reg query … /ve` output like
/// ` (Default) REG_SZ C:\Program Files\ArtCraft\PhotoCraft\photocraft.exe`.
#[cfg_attr(not(any(windows, test)), allow(dead_code))]
pub(crate) fn registered_path(text: &str) -> Option<std::path::PathBuf> {
text.lines().find_map(|l| l.split_once("REG_SZ")).map(|(_, v)| v.trim().trim_matches('"')).filter(|v| !v.is_empty()).map(std::path::PathBuf::from)
}
/// Is `app` an application Edit Original may start: an `.app` bundle on macOS, an `.exe` on
@@ -440,7 +506,7 @@ fn executable_file(p: &std::path::Path) -> bool {
#[cfg(test)]
mod open_with_tests {
use super::{check_app, find_photocraft, photocraft_candidates};
use super::{check_app, find_photocraft, photocraft_candidates, registered_path, windows_candidates};
/// A fresh, empty folder under the system's temporary folder.
fn scratch(name: &str) -> std::path::PathBuf {
@@ -503,4 +569,39 @@ mod open_with_tests {
}
let _ = std::fs::remove_dir_all(&d);
}
/// #5327 review: on Windows PhotoCraft is looked for where its installer puts it: the path it
/// registers, then `ArtCraft\PhotoCraft` in the per-user and per-machine program folders, then
/// the `PATH`.
#[test]
fn windows_looks_where_the_photocraft_installer_puts_it() {
use std::path::PathBuf;
let registered = vec![PathBuf::from("D:/Tools/PhotoCraft/photocraft.exe")];
let folders = [PathBuf::from("C:/Users/me/AppData/Local/Programs"), PathBuf::from("C:/Program Files"), PathBuf::from("C:/Program Files")];
let on_path = vec![PathBuf::from("C:/bin/photocraft.exe")];
let exe = |f: &str| PathBuf::from(f).join("ArtCraft").join("PhotoCraft").join("photocraft.exe");
assert_eq!(
windows_candidates(registered, &folders, on_path),
[
PathBuf::from("D:/Tools/PhotoCraft/photocraft.exe"),
exe("C:/Users/me/AppData/Local/Programs"),
exe("C:/Program Files"),
PathBuf::from("C:/bin/photocraft.exe"),
]
);
}
/// The registered path is read from `reg query … /ve` output, spaces and quotes included;
/// no value, no path.
#[test]
fn the_registered_path_is_read_from_reg_query() {
let out = "\r\nHKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\photocraft.exe\r\n (Default) REG_SZ C:\\Program Files\\ArtCraft\\PhotoCraft\\photocraft.exe\r\n";
assert_eq!(registered_path(out), Some(std::path::PathBuf::from(r"C:\Program Files\ArtCraft\PhotoCraft\photocraft.exe")));
assert_eq!(
registered_path(" (Standard) REG_SZ \"C:\\A B\\photocraft.exe\""),
Some(std::path::PathBuf::from(r"C:\A B\photocraft.exe"))
);
assert_eq!(registered_path("ERROR: The system was unable to find the specified registry key or value."), None);
assert_eq!(registered_path(" (Default) REG_SZ "), None);
}
}
+3 -2
View File
@@ -670,8 +670,9 @@ the system and return `{path}`. Edit Original opens only a regular file whose ex
reads, and answers a control request with an error unless the file is a placed document (which opens in a tab).
The preference `editOriginalSystemDefaults` (Preferences › File Handling › Use System Defaults for Edit Original, on
by default, #5236) opens the file in the system's default app for it; off, in the app that made it: a raster image in
PhotoCraft, looked for only where it installs (the Applications folders on macOS, the `PATH` elsewhere), and any other
file, or a raster image without PhotoCraft installed, in the system's default app.
PhotoCraft, looked for only where it installs (the Applications folders on macOS; on Windows the path its installer
registers under App Paths, then `ArtCraft\PhotoCraft` in the per-user and per-machine program folders, then the `PATH`;
the `PATH` elsewhere), and any other file, or a raster image without PhotoCraft installed, in the system's default app.
Package: File → Package… (`ui.packageDialog`) opens the `package` dialog for a saved document (a document never saved
gets a `confirm` dialog whose OK runs `file.saveAs`). Fields: `folder` (the location), `name` (the package folder),