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 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-07-10 16:41:34 +03:00
1 parent 256f9a282b
commit dc7117f203
12 files changed
+228 -411

No files matched your search

+8 -44
View File
@@ -6,10 +6,10 @@
//! survives across sessions.
use rustc_hash::FxHashSet as HashSet;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
/// Identifies a toggleable status-bar pill.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)]
pub enum StatusPill {
Coords,
Ortho,
@@ -99,13 +99,12 @@ impl StatusPill {
}
}
fn from_id(s: &str) -> Option<StatusPill> {
StatusPill::ALL.iter().copied().find(|p| p.id() == s)
}
}
/// Tracks which pills the user has hidden.
#[derive(Clone)]
/// Tracks which pills the user has hidden. Serialized as the "statusbar" section
/// of the app config ([`crate::app::config`]).
#[derive(Clone, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct StatusBarConfig {
hidden: HashSet<StatusPill>,
}
@@ -133,50 +132,15 @@ impl Default for StatusBarConfig {
}
impl StatusBarConfig {
/// Load the saved customization. When no config file exists yet, fall back
/// to the shipped defaults ([`StatusBarConfig::default`]); an existing file
/// (even an empty one — the user showed every pill) is authoritative.
pub fn load() -> Self {
match config_path().and_then(|p| std::fs::read_to_string(p).ok()) {
Some(body) => {
let hidden = body
.lines()
.filter_map(|l| StatusPill::from_id(l.trim()))
.collect();
Self { hidden }
}
None => Self::default(),
}
}
pub fn is_visible(&self, pill: StatusPill) -> bool {
!self.hidden.contains(&pill)
}
/// Flip a pill's visibility and persist the change.
/// Flip a pill's visibility. Persistence is handled by the caller via the
/// consolidated app config (`save_config`).
pub fn toggle(&mut self, pill: StatusPill) {
if !self.hidden.remove(&pill) {
self.hidden.insert(pill);
}
self.save();
}
fn save(&self) {
let Some(path) = config_path() else { return };
if let Some(dir) = path.parent() {
let _ = std::fs::create_dir_all(dir);
}
let body: String = StatusPill::ALL
.iter()
.filter(|p| self.hidden.contains(p))
.map(|p| p.id())
.collect::<Vec<_>>()
.join("\n");
let _ = std::fs::write(path, body);
}
}
/// `<config-dir>/OpenCADStudio/statusbar.txt`, matching the recent-files store.
fn config_path() -> Option<PathBuf> {
Some(crate::config::config_dir()?.join("statusbar.txt"))
}