Fix 26 bugs, edge cases, and consistency issues from fifth audit pass

Critical: undo toast now trashes only batch output files (not entire dir),
JPEG scanline write errors propagated, selective metadata write result returned.

High: zero-dimension guards in ResizeConfig/fit_within, negative aspect ratio
rejection, FM integration toggle infinite recursion guard, saturating counter
arithmetic in executor.

Medium: PNG compression level passed to oxipng, pct mode updates job_config,
external file loading updates step indicator, CLI undo removes history entries,
watch config write failures reported, fast-copy path reads image dimensions for
rename templates, discovery excludes unprocessable formats (heic/svg/ico/jxl),
CLI warns on invalid algorithm/overwrite values, resolve_collision trailing dot
fix, generation guards on all preview threads to cancel stale results, default
DPI aligned to 0, watermark text width uses char count not byte length.

Low: binary path escaped in Nautilus extension, file dialog filter aligned with
discovery, reset_wizard clears preset_mode and output_dir.
This commit is contained in:
2026-03-07 19:47:23 +02:00
parent 270a7db60d
commit b432cc7431
44 changed files with 5748 additions and 2221 deletions

View File

@@ -8,10 +8,19 @@ use crate::preset::Preset;
fn default_config_dir() -> PathBuf {
dirs::config_dir()
.unwrap_or_else(|| PathBuf::from("~/.config"))
.or_else(|| dirs::home_dir().map(|h| h.join(".config")))
.unwrap_or_else(std::env::temp_dir)
.join("pixstrip")
}
/// Write to a temporary file then rename, for crash safety.
fn atomic_write(path: &Path, contents: &str) -> std::io::Result<()> {
let tmp = path.with_extension("tmp");
std::fs::write(&tmp, contents)?;
std::fs::rename(&tmp, path)?;
Ok(())
}
fn sanitize_filename(name: &str) -> String {
name.chars()
.map(|c| match c {
@@ -54,7 +63,7 @@ impl PresetStore {
let path = self.preset_path(&preset.name);
let json = serde_json::to_string_pretty(preset)
.map_err(|e| PixstripError::Preset(e.to_string()))?;
std::fs::write(&path, json).map_err(PixstripError::Io)
atomic_write(&path, &json).map_err(PixstripError::Io)
}
pub fn load(&self, name: &str) -> Result<Preset> {
@@ -103,7 +112,7 @@ impl PresetStore {
pub fn export_to_file(&self, preset: &Preset, path: &Path) -> Result<()> {
let json = serde_json::to_string_pretty(preset)
.map_err(|e| PixstripError::Preset(e.to_string()))?;
std::fs::write(path, json).map_err(PixstripError::Io)
atomic_write(path, &json).map_err(PixstripError::Io)
}
pub fn import_from_file(&self, path: &Path) -> Result<Preset> {
@@ -146,7 +155,7 @@ impl ConfigStore {
}
let json = serde_json::to_string_pretty(config)
.map_err(|e| PixstripError::Config(e.to_string()))?;
std::fs::write(&self.config_path, json).map_err(PixstripError::Io)
atomic_write(&self.config_path, &json).map_err(PixstripError::Io)
}
pub fn load(&self) -> Result<AppConfig> {
@@ -215,7 +224,7 @@ impl SessionStore {
}
let json = serde_json::to_string_pretty(state)
.map_err(|e| PixstripError::Config(e.to_string()))?;
std::fs::write(&self.session_path, json).map_err(PixstripError::Io)
atomic_write(&self.session_path, &json).map_err(PixstripError::Io)
}
pub fn load(&self) -> Result<SessionState> {
@@ -267,10 +276,11 @@ impl HistoryStore {
}
}
pub fn add(&self, entry: HistoryEntry) -> Result<()> {
pub fn add(&self, entry: HistoryEntry, max_entries: usize, max_days: u32) -> Result<()> {
let mut entries = self.list()?;
entries.push(entry);
self.write_all(&entries)
self.write_all(&entries)?;
self.prune(max_entries, max_days)
}
pub fn prune(&self, max_entries: usize, max_days: u32) -> Result<()> {
@@ -285,9 +295,9 @@ impl HistoryStore {
.as_secs();
let cutoff_secs = now_secs.saturating_sub(max_days as u64 * 86400);
// Remove entries older than max_days
// Remove entries older than max_days (keep entries with unparseable timestamps)
entries.retain(|e| {
e.timestamp.parse::<u64>().unwrap_or(0) >= cutoff_secs
e.timestamp.parse::<u64>().map_or(true, |ts| ts >= cutoff_secs)
});
// Trim to max_entries (keep the most recent)
@@ -314,13 +324,13 @@ impl HistoryStore {
self.write_all(&Vec::<HistoryEntry>::new())
}
fn write_all(&self, entries: &[HistoryEntry]) -> Result<()> {
pub fn write_all(&self, entries: &[HistoryEntry]) -> Result<()> {
if let Some(parent) = self.history_path.parent() {
std::fs::create_dir_all(parent).map_err(PixstripError::Io)?;
}
let json = serde_json::to_string_pretty(entries)
.map_err(|e| PixstripError::Config(e.to_string()))?;
std::fs::write(&self.history_path, json).map_err(PixstripError::Io)
atomic_write(&self.history_path, &json).map_err(PixstripError::Io)
}
}