From 2e5a9f92ca701c160a186a3909713c5b913ec1e2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 12:05:13 +0000 Subject: [PATCH] LR-EXP-PSD: settings path fields use TextField; Windows default editor via Explorer; PackBits encoder without indexing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Settings ▸ General ▸ External Editing: the PhotoCraft location and Other editor fields move from egui::TextEdit to TextField (edit menu, Esc restores). - Edit In with the system default on Windows starts explorer.exe with the file as one argument instead of cmd /C start, which read & ^ % | in file names as shell syntax. - psd::packbits walks the row with first()/get()/split_at_checked instead of direct indexing and slicing. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BKWB9TCnvzJHV6xMLAj2J4 --- apps/lightcraft/src/editors.rs | 26 ++++++++++++++++++----- crates/codecs/src/psd.rs | 30 +++++++++++++-------------- crates/ui-egui/src/panels/settings.rs | 15 +++++++------- 3 files changed, 43 insertions(+), 28 deletions(-) diff --git a/apps/lightcraft/src/editors.rs b/apps/lightcraft/src/editors.rs index 8b246a5f8..9a8cc22d2 100644 --- a/apps/lightcraft/src/editors.rs +++ b/apps/lightcraft/src/editors.rs @@ -36,11 +36,7 @@ pub fn open_with(path: &str, app: &str) -> Result<(), String> { c.arg(path); c } - Launcher::Default if cfg!(target_os = "windows") => { - let mut c = Command::new("cmd"); - c.args(["/C", "start", "", path]); - c - } + Launcher::Default if cfg!(target_os = "windows") => windows_default_command(path), Launcher::Default => { let mut c = Command::new("xdg-open"); c.arg(path); @@ -65,6 +61,15 @@ pub fn open_with(path: &str, app: &str) -> Result<(), String> { c.spawn().map(|_| ()).map_err(|e| e.to_string()) } +/// Windows: open `path` in its default application through Explorer, given the path as its one +/// argument. Not `cmd /C start`: cmd.exe would read `&`, `^`, `%` or `|` in a file name as its own +/// syntax (running what follows an `&` as a command). +fn windows_default_command(path: &str) -> Command { + let mut c = Command::new("explorer.exe"); + c.arg(path.replace('/', "\\")); + c +} + fn launcher(app: &str) -> Result { let app = app.trim(); if app.is_empty() { @@ -175,6 +180,17 @@ fn desktop_exec(entry: &str) -> Option { mod tests { use super::*; + /// A file name with cmd.exe syntax in it goes to Explorer as one plain argument, not + /// through a shell. + #[test] + fn windows_default_open_goes_through_explorer_not_cmd() { + let path = r"C:/Edits/a & b ^c %PATH% | d.psd"; + let c = windows_default_command(path); + assert_eq!(c.get_program(), "explorer.exe"); + let args: Vec<_> = c.get_args().map(|a| a.to_string_lossy().into_owned()).collect(); + assert_eq!(args, [r"C:\Edits\a & b ^c %PATH% | d.psd"]); + } + #[test] fn windows_lookup_prefers_the_recorded_install_folder() { let env = |k: &str| match k { diff --git a/crates/codecs/src/psd.rs b/crates/codecs/src/psd.rs index 10e0819b6..7cccec0f8 100644 --- a/crates/codecs/src/psd.rs +++ b/crates/codecs/src/psd.rs @@ -316,26 +316,24 @@ fn resource_section(meta: &EncodeMeta) -> Vec { /// PackBits: append one encoded row of `src` to `out` (runs of 2+ equal bytes as repeats, the /// rest as literals of at most 128 bytes). fn packbits(src: &[u8], out: &mut Vec) { - let mut i = 0; - while i < src.len() { - let b = src[i]; - let mut run = 1; - while run < 128 && i + run < src.len() && src[i + run] == b { - run += 1; - } + let mut rest = src; + while let Some(&b) = rest.first() { + // a run of 2 to 128 equal bytes + let run = rest.iter().take(128).take_while(|&&x| x == b).count(); if run >= 2 { - out.push((1i16 - run as i16) as i8 as u8); + // 1 - run in 2..=128: -1..=-127, as a signed byte + out.push((1 - run as i16) as i8 as u8); out.push(b); - i += run; + rest = rest.get(run..).unwrap_or_default(); continue; } - // a literal: up to the next pair of equal bytes - let start = i; - while i < src.len() && i - start < 128 && !(i + 1 < src.len() && src[i] == src[i + 1]) { - i += 1; - } - out.push((i - start - 1) as u8); - out.extend_from_slice(&src[start..i]); + // a literal of 1 to 128 bytes: up to the next pair of equal bytes, or the end of the row + let unpaired = rest.windows(2).take(128).take_while(|w| w.first() != w.get(1)).count(); + let len = if unpaired.saturating_add(1) == rest.len() { rest.len() } else { unpaired }; + let (lit, tail) = rest.split_at_checked(len.clamp(1, 128)).unwrap_or((rest, &[][..])); + out.push(lit.len().saturating_sub(1) as u8); + out.extend_from_slice(lit); + rest = tail; } } diff --git a/crates/ui-egui/src/panels/settings.rs b/crates/ui-egui/src/panels/settings.rs index 75c00706e..86da2cc47 100644 --- a/crates/ui-egui/src/panels/settings.rs +++ b/crates/ui-egui/src/panels/settings.rs @@ -145,10 +145,10 @@ fn general_tab(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens) { check(ui, "settings.confirmDelete", &mut app.ui.settings.confirm_delete, "Confirm before moving photos to Recently Deleted"); heading(ui, t, crate::i18n::tr("External Editing")); row(ui, t, crate::i18n::tr("PhotoCraft"), |ui| { - let r = ui.add( - egui::TextEdit::singleline(&mut app.ui.settings.photocraft_path).hint_text(crate::i18n::tr("Found automatically")).desired_width(220.0), - ); - register(ui.ctx(), "field:photocraftPath", r.rect); + crate::text_field::TextField::singleline("field:photocraftPath", &mut app.ui.settings.photocraft_path) + .hint(crate::i18n::tr("Found automatically")) + .width(220.0) + .show(ui); }); edit_file_rows(ui, t, "photocraftFile", &mut app.ui.settings.photocraft_file); hint( @@ -159,9 +159,10 @@ fn general_tab(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens) { ), ); row(ui, t, crate::i18n::tr("Other editor"), |ui| { - let r = ui - .add(egui::TextEdit::singleline(&mut app.ui.settings.external_editor).hint_text(crate::i18n::tr("System default")).desired_width(220.0)); - register(ui.ctx(), "field:externalEditor", r.rect); + crate::text_field::TextField::singleline("field:externalEditor", &mut app.ui.settings.external_editor) + .hint(crate::i18n::tr("System default")) + .width(220.0) + .show(ui); }); edit_file_rows(ui, t, "externalFile", &mut app.ui.settings.external_file); hint(ui, t, crate::i18n::tr("Edit in Other Editor (⌥⌘E) opens the copy in this application (an app name on macOS, a program path elsewhere)."));