Windows: fix "Show in Explorer" opening Documents instead of the app's folder (#54)

* Fix Show in Explorer opening Documents for paths with spaces

reveal() built `/select,<path>` as one argument and passed it with
Command::arg, which quotes any argument containing a space. The Craft
apps' MSIs install under C:\Program Files, so Explorer received
"/select,C:\Program Files\...\app.exe", didn't recognise the switch and
opened its default folder, Documents.

The argument is now `/select,"<path>"`, with only the path quoted and
forward slashes turned into backslashes, passed with raw_arg so Rust
leaves it alone. The argument building is a plain function with unit
tests that run on every platform.

Fixes #39

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Refuse a path with a quote in it; give the Explorer tests their own module

A " would end the quoted /select path early and pass the rest to Explorer as
switches. The module name clashed with the pgrep tests that landed in #81.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
Fabio Lussi 2026-10-11 09:59:59 +02:00 committed by GitHub
parent 92e23e0348
commit 5b7d4781f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 58 additions and 5 deletions

View File

@ -160,16 +160,45 @@ pub fn reveal(path: &Path) -> Result<(), String> {
cmd.arg("-R").arg(path);
spawn_detached(cmd)
} else if cfg!(windows) {
let mut arg = std::ffi::OsString::from("/select,");
arg.push(path);
let mut cmd = command("explorer");
cmd.arg(arg);
spawn_detached(cmd)
reveal_in_explorer(path)
} else {
open(path.parent().unwrap_or(path).as_os_str())
}
}
#[cfg(windows)]
fn reveal_in_explorer(path: &Path) -> Result<(), String> {
use std::os::windows::process::CommandExt;
let mut cmd = command("explorer");
// Passed raw: `arg` would wrap all of `/select,...` in quotes when the path has a space
// (`C:\Program Files\...`), and Explorer then ignores the switch and opens Documents.
// A quote in the path would end the quoted part early and let the rest reach Explorer as
// switches; no Windows path has one, so refuse it.
let arg = explorer_select_arg(path).ok_or_else(|| format!("{} isn't a usable path", path.display()))?;
cmd.raw_arg(arg);
spawn_detached(cmd)
}
#[cfg(not(windows))]
fn reveal_in_explorer(_path: &Path) -> Result<(), String> {
Err("Explorer is only on Windows".into())
}
/// Explorer's `/select,"<path>"` argument as it goes on the command line: only the path is quoted,
/// and forward slashes become backslashes, which Explorer needs to find the item. `None` for a path
/// with a `"` in it.
#[cfg_attr(not(windows), allow(dead_code))]
fn explorer_select_arg(path: &Path) -> Option<std::ffi::OsString> {
let s = path.to_str()?;
if s.contains('"') {
return None;
}
let mut arg = std::ffi::OsString::from("/select,\"");
arg.push(s.replace('/', "\\"));
arg.push("\"");
Some(arg)
}
/// Whether a process started from inside `path` (an exe, an AppImage, or a `.app`) is running.
pub fn is_running(path: &Path) -> bool {
if cfg!(windows) {
@ -252,3 +281,27 @@ mod tests {
}
}
}
#[cfg(test)]
mod explorer_tests {
use super::*;
#[test]
fn explorer_select_quotes_only_the_path() {
let arg = explorer_select_arg(Path::new(r"C:\Program Files\PhotoCraft\photocraft.exe"));
assert_eq!(arg.unwrap(), r#"/select,"C:\Program Files\PhotoCraft\photocraft.exe""#);
}
#[test]
fn explorer_select_without_spaces() {
let arg = explorer_select_arg(Path::new(r"C:\Apps\photocraft.exe"));
assert_eq!(arg.unwrap(), r#"/select,"C:\Apps\photocraft.exe""#);
assert_eq!(explorer_select_arg(Path::new(r#"C:\Apps" /root,C:\x.exe"#)), None);
}
#[test]
fn explorer_select_uses_backslashes() {
let arg = explorer_select_arg(Path::new("C:/Users/A B/AppData/Local/ArtCraft Launcher/apps/photocraft.exe"));
assert_eq!(arg.unwrap(), r#"/select,"C:\Users\A B\AppData\Local\ArtCraft Launcher\apps\photocraft.exe""#);
}
}