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

+3 -35
View File
@@ -54,7 +54,9 @@ const MAX_PANEL_SQUEEZE: f32 = 8.0;
/// How the ribbon tool panels are sized. `Auto` adapts to the window width (the
/// step-by-step degradation); the others pin every panel to one density so the
/// user can override the automatic choice. The selection is persisted.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
#[derive(
Clone, Copy, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize,
)]
pub enum CollapseMode {
/// Size panels to the window: degrade from the right as space runs out.
#[default]
@@ -86,19 +88,6 @@ impl CollapseMode {
}
}
/// Stable identifier used for persistence.
fn id(self) -> &'static str {
match self {
CollapseMode::Auto => "auto",
CollapseMode::Full => "full",
CollapseMode::Compact => "compact",
CollapseMode::Collapsed => "collapsed",
}
}
fn from_id(s: &str) -> Option<Self> {
CollapseMode::ALL.iter().copied().find(|m| m.id() == s)
}
/// The degradation level every panel is pinned to, or `None` for `Auto`.
fn forced_level(self) -> Option<u8> {
@@ -110,27 +99,6 @@ impl CollapseMode {
}
}
/// Load the saved mode, defaulting to `Auto`.
pub fn load() -> Self {
config_path()
.and_then(|p| std::fs::read_to_string(p).ok())
.and_then(|s| CollapseMode::from_id(s.trim()))
.unwrap_or_default()
}
/// Persist this mode (best-effort; silent on failure).
pub fn save(self) {
let Some(path) = config_path() else { return };
if let Some(dir) = path.parent() {
let _ = std::fs::create_dir_all(dir);
}
let _ = std::fs::write(path, self.id());
}
}
/// `<config-dir>/OpenCADStudio/ribbon.txt`, matching the other settings stores.
fn config_path() -> Option<std::path::PathBuf> {
Some(crate::config::config_dir()?.join("ribbon.txt"))
}
impl std::fmt::Display for CollapseMode {
+8 -3
View File
@@ -161,15 +161,20 @@ impl Ribbon {
active_table_style: String::new(),
tab_bar_h: Arc::new(AtomicU32::new(28.0f32.to_bits())),
tool_bar_h: Arc::new(AtomicU32::new(TOOL_BAR_H.to_bits())),
collapse_mode: CollapseMode::load(),
collapse_mode: CollapseMode::default(),
collapse_tight: Arc::new(AtomicBool::new(false)),
}
}
/// Change the tool-panel density and persist the choice.
/// Change the tool-panel density. Persistence is handled by the caller via
/// the consolidated app config (`save_config`).
pub fn set_collapse_mode(&mut self, mode: CollapseMode) {
self.collapse_mode = mode;
mode.save();
}
/// The current tool-panel density (for saving into the app config).
pub fn collapse_mode(&self) -> CollapseMode {
self.collapse_mode
}
/// Current tab-bar height as last measured by the `WrapBar` widget.
+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"))
}
+17 -92
View File
@@ -93,22 +93,34 @@ pub enum PlotDlgMsg {
/// Transient state backing the Plot dialog. Seeded from the layout's plot
/// settings when the dialog opens; consumed on commit.
#[derive(Debug, Clone)]
// The persisted fields form the "plot" section of the app config
// ([`crate::app::config`]); `#[serde(skip)]` marks the runtime-only fields
// (discovered printers, live page/offset choices, name-entry state) so only the
// user's print preferences are written, matching the former plot.txt subset.
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct PlotDialogState {
/// Printer names discovered on the system (via `lpstat`), never the
/// sentinels.
#[serde(skip)]
pub printers: Vec<String>,
/// Chosen printer name, or `None` for the system default.
pub printer: Option<String>,
/// Output goes to a PDF file instead of a printer.
pub to_file: bool,
#[serde(skip)]
pub paper: String,
#[serde(skip)]
pub orientation: String,
#[serde(skip)]
pub rotation: String,
pub copies: String,
pub area: String,
#[serde(skip)]
pub center: bool,
#[serde(skip)]
pub offset_x: String,
#[serde(skip)]
pub offset_y: String,
pub scale: String,
pub scale_lw: bool,
@@ -126,12 +138,16 @@ pub struct PlotDialogState {
/// Display name of the active plot style table ("" = none).
pub style_name: String,
/// Named page setups in the document (refreshed when the dialog opens).
#[serde(skip)]
pub page_setups: Vec<String>,
/// Currently selected named page setup ("" = none / current layout).
#[serde(skip)]
pub selected_setup: String,
/// When `Some`, a name-entry row is showing (for New / Rename).
#[serde(skip)]
pub name_input: Option<String>,
/// `true` when `name_input` is renaming the selected setup, else creating.
#[serde(skip)]
pub name_rename: bool,
}
@@ -202,97 +218,6 @@ impl PlotDialogState {
self.style_name = o.style_name.clone();
}
/// Load the persisted print preferences (printer, copies, quality, output
/// options) from `<config>/OpenCADStudio/plot.txt`. Drawing-specific fields
/// (paper, scale, offset, rotation…) are NOT persisted here — they are
/// seeded from the active layout each time the dialog opens.
pub fn load() -> Self {
#[cfg(target_arch = "wasm32")]
{
Self::default()
}
#[cfg(not(target_arch = "wasm32"))]
{
let mut s = Self::default();
if let Some(body) = prefs_path().and_then(|p| std::fs::read_to_string(p).ok()) {
let flag = |v: &str| v == "1";
for line in body.lines() {
let Some((k, v)) = line.split_once('=') else {
continue;
};
let (k, v) = (k.trim(), v.trim());
match k {
"printer" => {
s.printer = if v.is_empty() { None } else { Some(v.to_string()) }
}
"to_file" => s.to_file = flag(v),
"area" => s.area = v.to_string(),
"scale" => s.scale = v.to_string(),
"copies" => s.copies = v.to_string(),
"quality" => s.quality = v.to_string(),
"dpi" => s.dpi = v.to_string(),
"shade" => s.shade = v.to_string(),
"mono" => s.mono = flag(v),
"lineweights" => s.lineweights = flag(v),
"with_styles" => s.with_styles = flag(v),
"transparency" => s.transparency = flag(v),
"paperspace_last" => s.paperspace_last = flag(v),
"hide_paperspace" => s.hide_paperspace = flag(v),
"stamp" => s.stamp = flag(v),
"save_layout" => s.save_layout = flag(v),
"scale_lw" => s.scale_lw = flag(v),
_ => {}
}
}
}
s
}
}
/// Best-effort persist of the print preferences (silent on failure).
#[cfg(not(target_arch = "wasm32"))]
pub fn save(&self) {
let Some(path) = prefs_path() else {
return;
};
if let Some(dir) = path.parent() {
let _ = std::fs::create_dir_all(dir);
}
let b = |v: bool| if v { "1" } else { "0" };
let printer = self.printer.clone().unwrap_or_default();
let body = format!(
"printer={}\nto_file={}\narea={}\nscale={}\ncopies={}\nquality={}\ndpi={}\nshade={}\nmono={}\n\
lineweights={}\nwith_styles={}\ntransparency={}\npaperspace_last={}\n\
hide_paperspace={}\nstamp={}\nsave_layout={}\nscale_lw={}\n",
printer,
b(self.to_file),
self.area,
self.scale,
self.copies,
self.quality,
self.dpi,
self.shade,
b(self.mono),
b(self.lineweights),
b(self.with_styles),
b(self.transparency),
b(self.paperspace_last),
b(self.hide_paperspace),
b(self.stamp),
b(self.save_layout),
b(self.scale_lw),
);
let _ = std::fs::write(path, body);
}
/// No-op persist on the web build (no filesystem).
#[cfg(target_arch = "wasm32")]
pub fn save(&self) {}
}
#[cfg(not(target_arch = "wasm32"))]
fn prefs_path() -> Option<std::path::PathBuf> {
Some(crate::config::config_dir()?.join("plot.txt"))
}
fn btn(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style {