Remember the name a library file was saved under

An .ase file can't say whose it is, so without this COM¹ saved after COM¹- wrote
over it. The app now remembers while it runs. After a restart COM¹ gets a new
file, which costs a copy and loses nothing.
This commit is contained in:
Adithya Shankaran
2026-11-06 23:05:29 +07:00
committed by bflatastic
parent 857ba3972b
commit a17efec894
2 changed files with 38 additions and 21 deletions
+13 -10
View File
@@ -6,7 +6,7 @@
//! pattern) into the document; `swatch.library.save` writes the document's ([`palette_io`]).
//! `swatch.library.copyToUser` copies a loaded library into the user library folder.
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use serde_json::{Value, json};
@@ -104,6 +104,9 @@ pub struct Libraries<L = SwatchLibrary> {
/// Loaded libraries copied into the user library folder ([`Libraries::copy_to_user`]): the
/// loaded id and the User Defined id [`Libraries::get`] finds for it.
copied: Vec<(String, String)>,
/// The name each User Defined file was saved under ([`Libraries::write`]), lower case id first:
/// a format that keeps no title (`.ase`) reads its name from the file name instead.
saved_as: HashMap<String, String>,
}
#[derive(Clone, Debug)]
@@ -152,6 +155,7 @@ impl<L: LibraryFile> Libraries<L> {
/// Set the user library folder and list its libraries.
pub fn set_user_dir(&mut self, dir: Option<String>) {
self.user_dir = dir;
self.saved_as.clear();
self.rescan();
}
@@ -218,16 +222,14 @@ impl<L: LibraryFile> Libraries<L> {
let device = crate::file_access::is_device_name;
let id = format!("user/{file}").to_lowercase();
let fallback = |other: &str| library_file_name(other, ext).to_lowercase() != file.to_lowercase();
// A format that keeps no title (`.ase`) reads its name from the file name, so the same
// library saved again looks like another one, and only the file name tells them apart.
let titled = L::read(bytes, stem(&file)).is_ok_and(|lib| lib.name() == name);
if titled
&& self.extra.iter().any(|e| {
e.info.id.to_lowercase() == id && e.lib.name() != name && (device(name) || device(e.lib.name()) || fallback(e.lib.name()))
})
{
let owner = |e: &Extra<L>| self.saved_as.get(&e.info.id.to_lowercase()).map_or(e.lib.name(), String::as_str).to_string();
let held = self.extra.iter().any(|e| {
let other = owner(e);
e.info.id.to_lowercase() == id && other != name && (device(name) || device(&other) || fallback(&other))
});
if held {
// Saved again, it goes back to the file it was given the first time.
let own = self.extra.iter().find(|e| e.info.category == "user" && e.lib.name() == name && extension(&e.info.id) == extension(&file));
let own = self.extra.iter().find(|e| e.info.category == "user" && owner(e) == name && extension(&e.info.id) == extension(&file));
file = match own.and_then(|e| e.info.id.strip_prefix("user/")) {
Some(own) => own.to_string(),
None => {
@@ -239,6 +241,7 @@ impl<L: LibraryFile> Libraries<L> {
let path = std::path::Path::new(&dir).join(&file).to_string_lossy().to_string();
// The app's own folder, under a name made safe: no root applies.
crate::file_access::unconfined(|| create_dir(&dir).and_then(|()| write_file(&path, bytes)))?;
self.saved_as.insert(format!("user/{file}").to_lowercase(), name.to_string());
self.rescan();
out["path"] = json!(path);
out["library"] = json!(format!("user/{file}"));
+25 -11
View File
@@ -792,18 +792,32 @@ fn a_library_named_like_a_device_keeps_the_library_with_its_file_name() {
}
}
/// `.ase` keeps no title, so a library named like a device is saved over its own file again, not
/// beside it.
/// `.ase` keeps no title, so a library's name comes back from its file name. Whichever is saved
/// first, `COM¹` and `COM¹-` keep a file each, and saving either again replaces only itself. After
/// a restart `COM¹` can't be told from `COM¹-`, and gets a new file, not the other's.
#[test]
fn a_device_named_library_without_a_title_is_not_copied_on_every_save() {
let dir = temp_dir("device-ase");
let mut s = session();
s.swatch_libraries.set_user_dir(Some(dir.to_string_lossy().to_string()));
let p = json!({"user": true, "name": "COM¹", "format": "ase"});
let first = run(&mut s, "swatch.library.save", p.clone())["library"].clone();
assert_eq!(run(&mut s, "swatch.library.save", p)["library"], first);
assert_eq!(std::fs::read_dir(&dir).unwrap().count(), 1);
let _ = std::fs::remove_dir_all(&dir);
fn device_named_libraries_without_a_title_keep_their_own_files() {
for (first, second) in [("COM¹", "COM¹-"), ("COM¹-", "COM¹")] {
let dir = temp_dir("device-ase");
let mut s = session();
s.swatch_libraries.set_user_dir(Some(dir.to_string_lossy().to_string()));
let save = |s: &mut Session, name: &str| {
run(s, "swatch.library.save", json!({"user": true, "name": name, "format": "ase"}))["library"].as_str().unwrap().to_string()
};
let a = save(&mut s, first);
assert_eq!(save(&mut s, first), a, "saving {first} again replaces {first}");
let b = save(&mut s, second);
assert_ne!(a, b);
assert_eq!(save(&mut s, first), a, "saving {first} again replaces {first}");
assert_eq!(save(&mut s, second), b, "saving {second} again replaces {second}");
assert_eq!(std::fs::read_dir(&dir).unwrap().count(), 2);
// The next time the app starts.
s.swatch_libraries.set_user_dir(Some(dir.to_string_lossy().to_string()));
let again = save(&mut s, "COM¹");
assert!(again != a && again != b, "{again}");
assert_eq!(std::fs::read_dir(&dir).unwrap().count(), 3);
let _ = std::fs::remove_dir_all(&dir);
}
}
/// #3602: a library titled with a Windows device name is saved under a name that is not one.