refactor(config): consolidate settings into one grouped settings.json

App preferences were spread across six flat plain-text files (settings.txt,
recent.txt, recent_limit.txt, statusbar.txt, ribbon.txt, plot.txt), each with a
bespoke line parser and no grouping. Replace them with a single grouped JSON,
serialized via serde (enabling the derive feature; serde_json was already a
dependency).

- New app/config.rs: AppConfig { settings, recent, statusbar, ribbon, plot } →
  <config>/settings.json (pretty JSON), load()/save().
- Make UserSettings, StatusBarConfig/StatusPill, CollapseMode and
  PlotDialogState serde-derived; #[serde(skip)] marks the plot dialog's runtime
  fields so only print preferences persist. Their bespoke .txt load/save is
  removed.
- App gains current_config/apply_config/save_config: one AppConfig::load at
  startup distributes into live state; every settings-change site
  (persist_settings_if_changed, recent add/remove/limit, status-bar pill
  toggle, ribbon density, plot commit) routes through save_config, which diffs
  against the last write to avoid thrashing.
- Clean start: the old flat files are ignored (no migration). ocad.pgp stays a
  separate hand-editable file.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-07-10 16:41:34 +03:00
commit dc7117f203
12 changed files with 227 additions and 410 deletions

View file

@ -1,7 +1,7 @@
//! Persistent recent-files store backing the Start page's Recent Documents
//! panel. Stored as plain text on disk (one path per line, newest first) next
//! to the other per-user config, so no serialization crate is pulled in just
//! for this.
//! Recent-files list backing the Start page's Recent Documents panel. The list
//! itself lives in the consolidated app config (`settings.json`, the "recent"
//! section); this module just mutates the in-memory list and persists via
//! `save_config`.
use super::OpenCADStudio;
use std::path::{Path, PathBuf};
@ -9,7 +9,7 @@ use std::path::{Path, PathBuf};
/// Bounds and default for how many recent files are kept.
pub(super) const RECENT_MIN: usize = 5;
pub(super) const RECENT_MAX: usize = 100;
const RECENT_DEFAULT: usize = 20;
pub(super) const RECENT_DEFAULT: usize = 20;
impl OpenCADStudio {
/// Record a freshly opened file at the top of the recents list.
@ -17,80 +17,20 @@ impl OpenCADStudio {
self.recent_files.retain(|r| r != &path);
self.recent_files.insert(0, path);
self.recent_files.truncate(self.recent_limit);
// Best-effort persist; silent on failure (read-only home, full disk).
let _ = save_recents(&self.recent_files);
self.save_config();
}
/// Drop a path from the recents list (manual removal from the Start page).
pub(super) fn remove_recent(&mut self, path: &Path) {
self.recent_files.retain(|r| r.as_path() != path);
let _ = save_recents(&self.recent_files);
self.save_config();
}
/// Set how many recent files are kept, trim the current list to fit, and
/// persist both the new limit and the trimmed list.
/// persist both.
pub(super) fn set_recent_limit(&mut self, limit: usize) {
self.recent_limit = limit.clamp(RECENT_MIN, RECENT_MAX);
self.recent_files.truncate(self.recent_limit);
save_recent_limit(self.recent_limit);
let _ = save_recents(&self.recent_files);
self.save_config();
}
}
/// Rehydrate the recents list from disk, trimmed to the saved limit. Call once
/// at app boot.
pub(super) fn load_recent_files() -> Vec<PathBuf> {
let Some(path) = recents_file_path() else {
return vec![];
};
let Ok(body) = std::fs::read_to_string(path) else {
return vec![];
};
let mut list: Vec<PathBuf> = body
.lines()
.filter(|l| !l.trim().is_empty())
.map(PathBuf::from)
.collect();
list.truncate(load_recent_limit());
list
}
/// Load the saved recent-file limit (clamped), defaulting to `RECENT_DEFAULT`.
pub(super) fn load_recent_limit() -> usize {
limit_file_path()
.and_then(|p| std::fs::read_to_string(p).ok())
.and_then(|s| s.trim().parse::<usize>().ok())
.map(|n| n.clamp(RECENT_MIN, RECENT_MAX))
.unwrap_or(RECENT_DEFAULT)
}
fn save_recent_limit(limit: usize) {
let Some(path) = limit_file_path() else { return };
if let Some(dir) = path.parent() {
let _ = std::fs::create_dir_all(dir);
}
let _ = std::fs::write(path, limit.to_string());
}
fn recents_file_path() -> Option<PathBuf> {
Some(crate::config::config_dir()?.join("recent.txt"))
}
fn limit_file_path() -> Option<PathBuf> {
Some(crate::config::config_dir()?.join("recent_limit.txt"))
}
fn save_recents(list: &[PathBuf]) -> std::io::Result<()> {
let Some(path) = recents_file_path() else {
return Ok(());
};
if let Some(dir) = path.parent() {
std::fs::create_dir_all(dir)?;
}
let body: String = list
.iter()
.map(|p| p.to_string_lossy().into_owned())
.collect::<Vec<_>>()
.join("\n");
std::fs::write(path, body)
}