diff --git a/apps/lightcraft/src/main.rs b/apps/lightcraft/src/main.rs index 854937b50..832b158b3 100644 --- a/apps/lightcraft/src/main.rs +++ b/apps/lightcraft/src/main.rs @@ -290,6 +290,39 @@ fn windows_open_url_command(url: &str) -> std::process::Command { c } +/// Explorer's `/select,` switch (issue #272). Explorer parses its own command line and only +/// selects the file when the path after the comma is quoted (`/select,"C:\My Photos\a.jpg"`), +/// spelled with backslashes and free of the verbatim prefix (`\\?\`, `\\?\UNC\`) that +/// `canonicalize` adds; given anything else it opens Documents. Windows file names can't contain +/// `"`; a path that does anyway (a hostile catalog) has them dropped, so the raw command line +/// always holds exactly this one argument. +fn explorer_select_arg(path: &str) -> String { + let path = path.replace('/', "\\").replace('"', ""); + let path = match path.strip_prefix(r"\\?\UNC\") { + Some(unc) => format!(r"\\{unc}"), + None => path.strip_prefix(r"\\?\").unwrap_or(path.as_str()).to_string(), + }; + format!("/select,\"{path}\"") +} + +/// `explorer /select,""`. Rust's own argument quoting wraps the whole switch in quotes +/// (`"/select,C:\My Photos\a.jpg"`) whenever the path has a space, which Explorer doesn't +/// recognise, so on Windows the switch goes on the command line exactly as built. +fn explorer_select_command(path: &str) -> std::process::Command { + let mut c = std::process::Command::new("explorer"); + let arg = explorer_select_arg(path); + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + c.raw_arg(arg); + } + #[cfg(not(windows))] + { + c.arg(arg); + } + c +} + /// The platform services; `ctx` repaints when a file dialog closes, and `log_file` is /// for Help ▸ Open Log Folder. fn services(ctx: egui::Context, log_file: Option<&std::path::Path>) -> Services { @@ -377,8 +410,7 @@ fn services(ctx: egui::Context, log_file: Option<&std::path::Path>) -> Services let status = if cfg!(target_os = "macos") { std::process::Command::new("open").args(["-R", path]).status() } else if cfg!(target_os = "windows") { - let win_path = path.replace('/', "\\"); - std::process::Command::new("explorer").arg(format!("/select,{win_path}")).status() + explorer_select_command(path).status() } else { let dir = std::path::Path::new(path).parent().map(|d| d.to_string_lossy().to_string()).unwrap_or_else(|| ".".into()); std::process::Command::new("xdg-open").arg(dir).status() @@ -928,6 +960,23 @@ mod tests { assert_eq!(args, ["url.dll,FileProtocolHandler", url]); } + /// Issue #272: Explorer gets the file to select quoted after `/select,`, with backslashes and + /// without the verbatim prefix, even when the path has spaces (else it opens Documents). + #[test] + fn explorer_select_quotes_the_path() { + assert_eq!(explorer_select_arg(r"C:\Users\Me\My Photos\IMG 1.jpg"), r#"/select,"C:\Users\Me\My Photos\IMG 1.jpg""#); + assert_eq!(explorer_select_arg("D:/Example/Photos/IMG_0001.CR3"), r#"/select,"D:\Example\Photos\IMG_0001.CR3""#); + assert_eq!(explorer_select_arg(r"\\server\share\My Photos\a.jpg"), r#"/select,"\\server\share\My Photos\a.jpg""#); + assert_eq!(explorer_select_arg(r"\\?\D:\My Photos\a.jpg"), r#"/select,"D:\My Photos\a.jpg""#); + assert_eq!(explorer_select_arg(r"\\?\UNC\server\share\a.jpg"), r#"/select,"\\server\share\a.jpg""#); + // a `"` can't end the quoted path and add arguments to the raw command line + assert_eq!(explorer_select_arg(r#"C:\a" "C:\b.jpg"#), r#"/select,"C:\a C:\b.jpg""#); + let c = explorer_select_command(r"C:\My Photos\a.jpg"); + assert_eq!(c.get_program(), "explorer"); + let args: Vec<&std::ffi::OsStr> = c.get_args().collect(); + assert_eq!(args, [r#"/select,"C:\My Photos\a.jpg""#]); + } + #[test] fn a_control_port_that_is_not_a_number_is_ignored() { assert_eq!(control_port_from("--control", Some("7980".into())), Some(7980)); diff --git a/crates/ui-egui/locales/ru.json b/crates/ui-egui/locales/ru.json index 382bfc05a..56ed57981 100644 --- a/crates/ui-egui/locales/ru.json +++ b/crates/ui-egui/locales/ru.json @@ -1103,6 +1103,8 @@ "Show destination gamut warning": "Показывать предупреждение о выходе за гамму устройства вывода", "Show display gamut warning": "Показывать предупреждение о выходе за гамму дисплея", "Show file names": "Показывать имена файлов", + "Show in Explorer": "Показать в Проводнике", + "Show in File Manager": "Показать в файловом менеджере", "Show in Finder": "Показать в Finder", "Show on Map": "Показать на карте", "Show overlay (O)": "Показать наложение (O)", diff --git a/crates/ui-egui/src/panels/left.rs b/crates/ui-egui/src/panels/left.rs index 8863e5d18..2676f59ca 100644 --- a/crates/ui-egui/src/panels/left.rs +++ b/crates/ui-egui/src/panels/left.rs @@ -1246,7 +1246,7 @@ fn folder_menu_for_library(app: &mut LightcraftApp, resp: &egui::Response, n: &F ui.close(); } if app.services.reveal.is_some() - && ui.button(crate::i18n::tr("Show in Finder")).clicked() + && ui.button(crate::i18n::tr(crate::menus::reveal_label())).clicked() && let Some(f) = app.services.reveal.as_mut() { let _ = f(path);