LR-EXP-PSD: settings path fields use TextField; Windows default editor via Explorer; PackBits encoder without indexing

- 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BKWB9TCnvzJHV6xMLAj2J4
This commit is contained in:
Claude
2026-10-11 12:05:13 +00:00
parent e1016acc0f
commit 2e5a9f92ca
3 changed files with 43 additions and 28 deletions
+21 -5
View File
@@ -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<Launcher, String> {
let app = app.trim();
if app.is_empty() {
@@ -175,6 +180,17 @@ fn desktop_exec(entry: &str) -> Option<PathBuf> {
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 {
+14 -16
View File
@@ -316,26 +316,24 @@ fn resource_section(meta: &EncodeMeta) -> Vec<u8> {
/// 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<u8>) {
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;
}
}
+8 -7
View File
@@ -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)."));