Browse / github.com/storytold/photocraft / patch
Windows: run explorer.exe and reg.exe by full path (#9666)
Windows: Reveal in Explorer and the locale lookup start explorer.exe / reg.exe from the app's folder **TL;DR:** On Windows, PhotoCraft starts `explorer` (Reveal in Explorer) and `reg` (UI language lookup for CJK fonts) by bare name, so an `explorer.exe` or `reg.exe` lying next to `photocraft.exe` runs instead of the Windows one. ### What goes wrong - `crates/engine/src/layer_menu_cmds.rs` (`reveal_command_on` / `spawn`): `Command::new("explorer")`. - `crates/text/src/cjk.rs` (`os_locale`, Windows): `Command::new("reg")`. Rust's `Command` on Windows looks in the application's own folder before System32. A portable PhotoCraft run from a folder that also holds other downloads (e.g. `Downloads`
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- davidmann4 (imported from pull request #9667, open)
- Base
7ac7b8b106acccf6826d45ed5c9e481dfe341035- Size
- +84 −12 in 3 files
- Review
- pending
- Shared
- 2026-10-28
- Id
b53bf4c6efc7c20f88373abc
How it went
No other operator has reported using it yet.
Diff
raw · crates/engine/src/cmd/file_cmds/tests.rs crates/engine/src/cmd/layer_menu_cmds.rs crates/text/src/cjk.rs
@@ -1531,10 +1531,26 @@ fn reveal_in_finder_on_windows_passes_the_clicked_tabs_saved_path() { s.execute("file.new", json!({"width": 8, "height": 8, "name": "other"})).unwrap(); let r = s.execute("file.revealInFinder", json!({"document": 0, "dryRun": true})).unwrap();- assert_eq!(r["program"], "explorer");+ // The Windows Explorer by full path: a bare name would first find an explorer.exe in the app's folder.+ let explorer = crate::layer_menu_cmds::windows_root(std::env::var_os("SystemRoot")).join("explorer.exe");+ assert_eq!(r["program"], json!(explorer.to_string_lossy())); assert_eq!(r["args"], json!(["/select,", r"C:\项目 图片\海报 🖼.png"])); } +/// Windows tools run from the Windows folder `SystemRoot` names, and from `C:\Windows` when it is+/// unset or relative (which would resolve against the working directory).+#[cfg(windows)]+#[test]+fn the_windows_folder_falls_back_when_system_root_is_unset_or_relative() {+ use crate::layer_menu_cmds::windows_root;+ use std::path::PathBuf;+ assert_eq!(windows_root(Some(r"D:\WinNT".into())), PathBuf::from(r"D:\WinNT"));+ assert_eq!(windows_root(None), PathBuf::from(r"C:\Windows"));+ assert_eq!(windows_root(Some(r"Windows".into())), PathBuf::from(r"C:\Windows"));+ assert_eq!(windows_root(Some(r"\Windows".into())), PathBuf::from(r"C:\Windows"));+ assert_eq!(windows_root(Some("".into())), PathBuf::from(r"C:\Windows"));+}+ /// "Include All Subfolders" (AUTO-219-5, #3753): Batch and Image Processor take the images in /// subfolders too, each result keeping its subfolder under the output folder (so same-named files /// don't collide), and skip the output folder when it sits inside the input folder.@@ -392,8 +392,8 @@ pub(crate) fn reveal(path: &str, dry_run: bool) -> Result<Value> { if dry_run { return Ok(json!({"program": program, "args": args})); }- ensure_folder(program, &args)?;- spawn(program, &args)?;+ ensure_folder(&program, &args)?;+ spawn(&program, &args)?; Ok(json!({"path": path})) } @@ -411,8 +411,28 @@ fn ensure_folder(program: &str, args: &[String]) -> Result<()> { } /// The file-manager invocation that selects `path` on this platform.-pub fn reveal_command(path: &str) -> Result<(&'static str, Vec<String>)> {- reveal_command_on(std::env::consts::OS, path)+pub fn reveal_command(path: &str) -> Result<(String, Vec<String>)> {+ let (program, args) = reveal_command_on(std::env::consts::OS, path)?;+ Ok((system_program(program), args))+}++/// Windows tools run by full path: by bare name, Windows first looks in the app's own folder, so+/// an `explorer.exe` lying next to a portable PhotoCraft would start instead (#9666).+#[cfg(windows)]+fn system_program(program: &str) -> String {+ windows_root(std::env::var_os("SystemRoot")).join(format!("{program}.exe")).to_string_lossy().into_owned()+}++/// The Windows folder for the `SystemRoot` value `system_root`: `C:\Windows` when it is unset or+/// not an absolute path (a relative one would resolve against the working directory).+#[cfg(windows)]+pub(crate) fn windows_root(system_root: Option<std::ffi::OsString>) -> std::path::PathBuf {+ system_root.map(std::path::PathBuf::from).filter(|p| p.is_absolute()).unwrap_or_else(|| std::path::PathBuf::from(r"C:\Windows"))+}++#[cfg(not(windows))]+fn system_program(program: &str) -> String {+ program.to_string() } /// [`reveal_command`] on the platform `os` (a `std::env::consts::OS` value).@@ -136,18 +136,30 @@ fn os_locale() -> Option<String> { #[cfg(target_os = "windows")] fn os_locale() -> Option<String> {- use std::os::windows::process::CommandExt;- const CREATE_NO_WINDOW: u32 = 0x0800_0000;- let out = std::process::Command::new("reg")- .args(["query", "HKCU\\Control Panel\\International", "/v", "LocaleName"])- .creation_flags(CREATE_NO_WINDOW)- .output()- .ok()?;+ let out = locale_query().output().ok()?; let text = String::from_utf8_lossy(&out.stdout); // " LocaleName REG_SZ ja-JP" text.lines().find(|l| l.contains("LocaleName")).and_then(|l| l.split_whitespace().last()).map(str::to_string) } +/// `reg query` for the UI locale, with `reg.exe` by full path: by bare name, Windows first looks in+/// the app's own folder, so a `reg.exe` lying next to a portable PhotoCraft would start (#9666).+#[cfg(target_os = "windows")]+fn locale_query() -> std::process::Command {+ use std::os::windows::process::CommandExt;+ const CREATE_NO_WINDOW: u32 = 0x0800_0000;+ let mut cmd = std::process::Command::new(windows_root(std::env::var_os("SystemRoot")).join("System32").join("reg.exe"));+ cmd.args(["query", "HKCU\\Control Panel\\International", "/v", "LocaleName"]).creation_flags(CREATE_NO_WINDOW);+ cmd+}++/// The Windows folder for the `SystemRoot` value `system_root`: `C:\Windows` when it is unset or+/// not an absolute path (a relative one would resolve against the working directory).+#[cfg(target_os = "windows")]+fn windows_root(system_root: Option<std::ffi::OsString>) -> std::path::PathBuf {+ system_root.map(std::path::PathBuf::from).filter(|p| p.is_absolute()).unwrap_or_else(|| std::path::PathBuf::from(r"C:\Windows"))+}+ #[cfg(not(any(target_os = "macos", target_os = "windows")))] fn os_locale() -> Option<String> { None@@ -654,6 +666,30 @@ mod tests { use super::*; use CjkScript::*; + /// `reg.exe` runs from System32, not from wherever a bare name would find one first (#9666),+ /// and the query still answers the UI locale.+ #[cfg(target_os = "windows")]+ #[test]+ fn locale_query_runs_the_system_reg() {+ let cmd = locale_query();+ let system = windows_root(std::env::var_os("SystemRoot")).join("System32").join("reg.exe");+ assert_eq!(std::path::Path::new(cmd.get_program()), system);+ assert!(os_locale().is_some_and(|l| !l.is_empty()));+ }++ /// `SystemRoot` unset or relative (which would resolve against the working directory) falls+ /// back to `C:\Windows`.+ #[cfg(target_os = "windows")]+ #[test]+ fn the_windows_folder_falls_back_when_system_root_is_unset_or_relative() {+ use std::path::PathBuf;+ assert_eq!(windows_root(Some(r"D:\WinNT".into())), PathBuf::from(r"D:\WinNT"));+ assert_eq!(windows_root(None), PathBuf::from(r"C:\Windows"));+ assert_eq!(windows_root(Some(r"Windows".into())), PathBuf::from(r"C:\Windows"));+ assert_eq!(windows_root(Some(r"\Windows".into())), PathBuf::from(r"C:\Windows"));+ assert_eq!(windows_root(Some("".into())), PathBuf::from(r"C:\Windows"));+ }+ #[test] fn explicit_locale_scripts_are_distinct_from_the_non_cjk_default() { assert_eq!(locale_script(Some("ja_JP.UTF-8")), Some(Japanese));