-
-
Notifications
You must be signed in to change notification settings - Fork 60
Settings Persistence
This page explains how Sucrose persists its configuration: the JSON file format, the three SettingManager implementations, the custom serializers, the mutex-guarded cross-process I/O, and the self-healing reset behavior. It is aimed at developers and power users who want to understand or safely hand-edit the on-disk settings. For where the files live see Data-Locations; for the catalog of keys and defaults see Settings-All-Keys.
- Mental model
- The three collaborating libraries
- JSON file format
- The SettingManager variants
- Public API surface
- Custom converters
- Cross-process safe I/O
- Self-healing and auto-reset
- Where keys and defaults live
- Hand-editing caveats
Sucrose stores all persistent configuration as a set of small JSON files, one file per topical category (General.json, Engine.json, Portal.json, …). There is no single monolithic config file, and settings are never stored in the registry (the only registry write is to Windows' own DirectX GPU-preference key — see Data-Locations). The files live under %AppData%\Sucrose\Setting\.
| Library | Role |
|---|---|
Sucrose.Memory |
Pure constant/readonly definitions — the strings: setting key names, folder names, file names, exe names, URLs, mutex names. No logic; the single source of truth for magic strings. |
Sucrose.Manager |
The persistence engine: SettingManager reads/writes the JSON files, LogManager writes logs, and the Manage.* static classes expose strongly-typed, defaulted, clamped accessors. |
Sucrose.Resources |
UI localization (one XAML ResourceDictionary per language) plus a resource-lookup helper. Not a settings store, but part of the shared-library set. |
See Shared-Item-Projects and Code-Conventions for how these libraries fit into the overall codebase.
Every settings file is a single JSON object with one property, Properties, a string→object dictionary:
{
"Properties": {
"Culture": "EN",
"AppVisible": true,
"WallpaperVolume": 100
}
}The internal model is private class Settings { public Dictionary<string, object> Properties { get; set; } }. Notable facts:
- The serializer is Newtonsoft.Json (
JsonConvert), withFormatting.Indentedby default andTypeNameHandling.None. - The JSON key name equals the setting constant value (e.g. the key
"Culture"is the constantConstant.General.Culture). - Enums are written as their string member name (not the integer) — see Custom converters.
There are three parallel implementations with the same public surface and identical JSON shape. A project picks one based on its concurrency needs:
| Class | File | Freshness / concurrency strategy |
|---|---|---|
SettingManager |
src/Library/Sucrose.Manager/SettingManager.cs |
Reloads from disk on read when File.GetLastWriteTime > _lastWrite. No lock. Provides GetSettingAddress. |
SettingManager2 |
src/Library/Sucrose.Manager/SettingManager2.cs |
Same freshness check plus a lock for thread safety. No GetSettingAddress. |
SettingManager3 |
src/Library/Sucrose.Manager/SettingManager3.cs |
Keeps an in-memory copy refreshed by a FileSystemWatcher (NotifyFilters.LastWrite); reads come from memory. Adds an explicit SaveSetting(); SetSetting upserts keys individually. |
The central registry Sucrose.Manager.Manage.Internal instantiates one SettingManager per JSON file (one LogManager per process log).
new SettingManager("General.json")
// → %AppData%\Sucrose\Setting\General.json
_settingsFilePath = Path.Combine(SMMRP.ApplicationData, SMMRG.AppName, SMMRF.Setting, settingsFileName);Where Path.ApplicationData is the Roaming AppData, General.AppName is "Sucrose", and Folder.Setting is "Setting". The directory is auto-created on construction.
flowchart LR
Acc["Manage.* accessor<br/>typed default + clamp"] --> Mgr["SettingManager<br/>freshness check / in-memory copy"]
Mgr --> RW["mutex-guarded Reader / Writer<br/>mutex name = MD5 of file path"]
RW --> File["%AppData%/Sucrose/Setting/<br/><Category>.json"]
File -. invalid or missing .-> Reset["auto-reset to empty Properties"]
Reset --> File
All three managers expose the same methods:
| Method | Purpose |
|---|---|
T GetSetting<T>(string key, T back = default) |
Typed read via ConvertToType<T> (handles IPAddress, Uri, Guid, enums, KeyValuePair<string,string> split on :, string[], List<string>, Dictionary<string,string>). |
T GetSettingStable<T>(string key, T back = default) |
Read via JsonConvert.DeserializeObject<T> of the value's string (used for numeric/value types that are then clamped). |
T GetSettingAddress<T>(...) |
(Managers 1 & 3 only) same as GetSetting, for address-like values. |
void SetSetting<T>(string key, T value) / void SetSetting<T>(KeyValuePair<string,T>[] pairs)
|
Single or batch write. |
string ReadSetting() |
Raw file text. |
void ApplySetting() |
Reset the file to empty { "Properties": {} }. |
void SaveSetting() |
(Manager 3 only) persist current in-memory state. |
bool CheckFile() |
Does the file exist. |
string SettingFile() |
Full path to the JSON file (handy for "open settings folder" UI). |
Two custom Newtonsoft converters are registered (in src/Library/Sucrose.Manager/Converter/):
| Converter | Behavior |
|---|---|
EnumConverter |
Writes enums as their string name; parses case-insensitively. |
IPAddressConverter |
Serializes an IPAddress as a plain string. |
Because of EnumConverter, any enum value in a settings file is stored as text (e.g. "UniformToFill", not 5). Hand-editing must use the enum member name.
Sucrose runs many processes that may touch the same file (Portal, the engines, Backgroundog, Commandog, …). I/O is therefore made safe across processes (Helper/Reader.cs, Helper/Writer.cs):
- All reads and writes are guarded by a named
Mutexwhose name is an MD5-derived GUID of the file path (Helper/Unique.cs→GenerateText). The same file path yields the same mutex name in every process. -
Reader.ReadStreamopens withFileShare.None;Writer.WriteStreamtruncates-or-creates. -
Helper/Cleaner.csstrips stray doubled braces ({{/}}) as a defensive cleanup applied to all read/written content. -
Helper/Validator.csvalidates JSON before a file is trusted.
This is the same persistence layer used by Backup-Restore-Reset. For the broader process-to-process communication (pipes, signals, transmission) see IPC.
The constructor runs a private ControlFile() step: if a settings file is missing, empty, or invalid JSON, it is automatically reset to { "Properties": {} } via ApplySetting(). As a result, a corrupted settings file does not crash the app — it silently reverts that category to defaults on next launch. This is why "delete the file" is a valid reset for a single category (see Backup-Restore-Reset) and why corrupt-settings symptoms self-resolve (see Troubleshooting-Settings-Startup-GPU).
Additionally, GetSettingStable numeric reads are clamped in the Manage.* accessors via Skylark.Helper.Skymath.Clamp. An out-of-range value in the JSON is silently clamped at read time — but the file is not rewritten, so the raw out-of-range value remains on disk until the next write of that key.
| Concern | Lives in | Example |
|---|---|---|
| Key name (the JSON property) |
Sucrose.Memory.Manage.Constant.* (const string) |
Constant.General.Culture → "Culture"
|
| Default value, range, clamp |
Sucrose.Manager.Manage.* accessor |
Manage.Engine.WallpaperVolume defaults to 100, clamped 0–100 |
| File the key belongs to | Sucrose.Manager.Manage.Internal |
GeneralSettingManager → General.json
|
The full enumerated catalog (key, type, default, range, file) is on Settings-All-Keys; per-page detail is on Settings-Overview and its sibling pages.
If you edit settings JSON by hand:
- Stop Sucrose first. Reads reload on file change, but a write from a running process can overwrite your edit (writes are mutex-guarded but not merge-aware).
-
Use enum member names, not integers (e.g.
"Mica", not a number) —EnumConverterexpects names. -
Keep the
{ "Properties": { … } }envelope. A file without it is treated as invalid and reset on next launch. -
Writes swallow all exceptions (
catch { }): a locked or permission-denied file silently fails to persist, so verify your change survived a restart. - Out-of-range numeric values are clamped at read time but left on disk; re-enter a valid value if you want the file itself corrected.
Getting Started
- Installation
- System Requirements
- Quick Start
- Portal Interface Tour
- Updating Sucrose
- Uninstalling Sucrose
Wallpaper Types
Using Sucrose
- Managing Library
- Using Store
- Customizing Wallpaper
- Multi-Monitor
- Wallpaper Cycling
- Choosing Engines
- Performance Rules
- Theme, Tray & Startup
- Discord Rich Presence
Settings Reference
- Settings Overview
- Settings: General
- Settings: Personal
- Settings: Performance
- Settings: Wallpaper
- Settings: System
- Settings: Other
- Settings: All Keys
Creating Wallpapers
- Create Overview
- Create: Step By Step
- Create: Package Format
- Create: Customization Controls
- Create: JS Bridge
- Create: Audio API
- Create: System API
- Create: Property Listener & Filters
- Create: Web Architecture
- Create: Compatibility
- Create: Example Wallpapers
- Create: Sharing & Publishing
Engine Reference
- Engines Overview
- Engine: MpvPlayer
- Engine: VlcPlayer
- Engine: WebView
- Engine: CefSharp
- Engine: Nebula
- Engine: Vexana
- Engine: Xavier
- Engine: Aurora
- Engine Comparison
Automation & Command Line
Architecture & Internals
- Architecture Overview
- Lifecycle
- Commandog Dispatcher
- Single-Instance Mutexes
- IPC
- Backgroundog Service
- Crash Reporting
- Update Internals
- Property Service
- Undo Internals
Data, Files & Diagnostics
Building & Contributing
- Building From Source
- Repository Layout
- Shared Item Projects
- Code Conventions
- Preprocessor Symbols
- Publish Pipeline
- Bundle Installer Internals
- Extending Sucrose
- Contributing
- Translating with Localizer
- Localization Coverage
- Security Policy
- Privacy & Telemetry
Help & Support