Files
Alejandro Londono 6006abb757 feat(settings): add shared config storage for Craft apps
Add shared config.toml storage while keeping settings types, defaults and UI policy in each app.

Includes isolated in-memory operation, configurable portable paths, atomic saves with private backups, damaged-file preservation, schema migrations, and documented best-effort handling of external edits.

Validation: native macOS formatting, strict workspace clippy, 172 workspace tests and the craft-settings WASM check. Independent backup-permission and failed external-migration repros fail before the fixes and pass afterward.

Hosted CI: all seven UI and printing jobs pass across macOS, Linux, Windows and UI WASM.

Refs #12
2026-10-12 15:40:50 -06:00

97 lines
2.8 KiB
Rust

#![allow(dead_code)]
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::time::Duration;
use craft_settings::{DocumentMut, Location, MigrateError, OpenOptions, Probe, Settings, SettingsStore};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Default, Clone, PartialEq, Debug)]
#[serde(default)]
pub struct Prefs {
pub general: General,
pub appearance: Appearance,
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
#[serde(default)]
pub struct General {
pub language: String,
pub autosave_minutes: u32,
pub recent: Vec<String>,
}
impl Default for General {
fn default() -> Self {
Self { language: "en".into(), autosave_minutes: 5, recent: Vec::new() }
}
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
#[serde(default)]
pub struct Appearance {
pub theme: Theme,
pub ui_scale: f64,
}
impl Default for Appearance {
fn default() -> Self {
Self { theme: Theme::Light, ui_scale: 1.0 }
}
}
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Debug)]
#[serde(rename_all = "lowercase")]
pub enum Theme {
Light,
Dark,
}
impl Settings for Prefs {
const APP_ID: &'static str = "testcraft";
const SCHEMA_VERSION: u32 = 2;
/// Version 1 stored the theme as `[appearance] dark = true`.
fn migrate(from: u32, doc: &mut DocumentMut) -> Result<(), MigrateError> {
match from {
1 => {
let dark = doc.get("appearance").and_then(|a| a.get("dark")).and_then(|d| d.as_bool());
if let Some(dark) = dark {
doc["appearance"]["theme"] = toml_edit::value(if dark { "dark" } else { "light" });
if let Some(table) = doc["appearance"].as_table_like_mut() {
table.remove("dark");
}
}
Ok(())
}
other => Err(MigrateError::new(other, "unknown version")),
}
}
}
/// A probe that sees no environment, no executable and no platform folder.
pub fn empty_probe() -> Probe {
Probe { env: Box::new(|_| None), exe_dir: None, platform_config_dir: None }
}
pub fn probe_with_env(name: &'static str, value: &'static str) -> Probe {
Probe { env: Box::new(move |n| (n == name).then(|| OsString::from(value))), exe_dir: None, platform_config_dir: None }
}
pub fn open_in(dir: &Path) -> (SettingsStore<Prefs>, craft_settings::LoadReport) {
SettingsStore::open_with(OpenOptions::new(Location::Dir(dir.to_path_buf())).probe(empty_probe()).debounce(Duration::from_millis(100)))
}
pub fn config_path(dir: &Path) -> PathBuf {
dir.join("config.toml")
}
pub fn write_config(dir: &Path, text: &str) {
std::fs::write(config_path(dir), text).unwrap();
}
pub fn read_config(dir: &Path) -> String {
std::fs::read_to_string(config_path(dir)).unwrap()
}