From 81b88332b59f16db5e64e59e4116cdde1c6ba0f7 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 13 Nov 2026 04:34:44 -0500 Subject: [PATCH] Share library and recent files JSON models --- crates/stencilcraft-document/src/lib.rs | 2 + crates/stencilcraft-document/src/metadata.rs | 85 ++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 crates/stencilcraft-document/src/metadata.rs diff --git a/crates/stencilcraft-document/src/lib.rs b/crates/stencilcraft-document/src/lib.rs index bca33fb8d..6e04be5b5 100644 --- a/crates/stencilcraft-document/src/lib.rs +++ b/crates/stencilcraft-document/src/lib.rs @@ -6,6 +6,7 @@ pub mod canvas; pub mod geom; +pub mod metadata; use crate::geom::{GeomError, bounds_of_shape, check_finite_mm}; pub use canvas::{Grid, Group, GroupChild, Guide, GuideAxis, Metadata, Orientation, Page, PagePreset, PageSetup, SnapSettings}; @@ -13,6 +14,7 @@ use kurbo::BezPath; use serde::{Deserialize, Serialize}; pub mod production; +pub use metadata::{LibraryIndex, LibraryTrashEntry, MAX_RECENT_FILES, RecentFiles}; pub use production::{CombineOp, GroupKind, Operation, SendAssign, ShapeProduction, SourceGroup}; /// Native format identity and supported version, defined by the shared wire-format crate. diff --git a/crates/stencilcraft-document/src/metadata.rs b/crates/stencilcraft-document/src/metadata.rs new file mode 100644 index 000000000..04fb24d83 --- /dev/null +++ b/crates/stencilcraft-document/src/metadata.rs @@ -0,0 +1,85 @@ +//! Small JSON records used by StencilCraft's local library and recent-files list. +//! +//! These records are separate from a document, but their on-disk field names, defaults and +//! JSON encoding are part of StencilCraft's supported persisted data. + +use std::path::Path; + +use serde::{Deserialize, Serialize}; + +use crate::DocError; + +/// One item in the local library recycle bin. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct LibraryTrashEntry { + /// File name inside the bin. + pub name: String, + /// Where it was, for restoring. + pub original: String, +} + +/// The `.library.json` record stored at the root of a StencilCraft library. +#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)] +pub struct LibraryIndex { + #[serde(default)] + pub favourites: Vec, + #[serde(default)] + pub trash: Vec, +} + +impl LibraryIndex { + /// Parse the library index, applying defaults for fields absent in older indexes. + pub fn from_json(json: &str) -> Result { + serde_json::from_str(json) + } + + /// Serialize the library index using the canonical pretty JSON representation. + pub fn to_json_pretty(&self) -> Result { + serde_json::to_string_pretty(self) + } +} + +/// Maximum number of paths retained in the recent-files record. +pub const MAX_RECENT_FILES: usize = 10; + +/// Most recently used document paths, newest first, without duplicates. +#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)] +pub struct RecentFiles { + #[serde(default)] + pub paths: Vec, +} + +impl RecentFiles { + /// Parse recent files from their persisted JSON representation. + pub fn from_json(json: &str) -> Result { + serde_json::from_str(json) + } + + /// Serialize recent files using the canonical pretty JSON representation. + pub fn to_json_pretty(&self) -> Result { + serde_json::to_string_pretty(self) + } + + /// Moves `path` to the front, keeping at most [`MAX_RECENT_FILES`]. + pub fn push(&mut self, path: &str) { + self.paths.retain(|p| p != path); + self.paths.insert(0, path.to_string()); + self.paths.truncate(MAX_RECENT_FILES); + } + + pub fn remove(&mut self, path: &str) { + self.paths.retain(|p| p != path); + } + + /// Reads the list; a missing or damaged file gives an empty list. + pub fn load(file: &Path) -> Self { + let mut list = std::fs::read_to_string(file).ok().and_then(|text| Self::from_json(&text).ok()).unwrap_or_default(); + list.paths.truncate(MAX_RECENT_FILES); + list + } + + pub fn save(&self, file: &Path) -> Result<(), DocError> { + let json = self.to_json_pretty().map_err(|e| DocError::Structure { path: file.display().to_string(), message: e.to_string() })?; + craft_atomic::atomic_write(file, json.as_bytes()).map_err(|source| DocError::Io { path: file.display().to_string(), source }) + } +}