Configuration Persistence
January 17, 2026 ยท View on GitHub
Overview
The configuration persistence module handles loading and saving application settings to platform-specific directories with robust error handling and graceful fallback to defaults.
Key Files
src/config/persistence.rs- Core persistence functionssrc/config/mod.rs- Module exportssrc/config/settings.rs- Settings struct being persisted
Implementation Details
Platform-Specific Directories
The module uses the dirs crate to determine the appropriate config directory for each platform:
| Platform | Config Directory |
|---|---|
| Windows | %APPDATA%\sleek-markdown-editor\ |
| macOS | ~/Library/Application Support/sleek-markdown-editor/ |
| Linux | ~/.config/sleek-markdown-editor/ |
Configuration is stored in config.json within this directory.
Public API
// Load config (graceful fallback to defaults)
pub fn load_config() -> Settings
// Load config with full error information
pub fn load_config_strict() -> Result<Settings>
// Save config (atomic write)
pub fn save_config(settings: &Settings) -> Result<()>
// Save config, ignoring errors (returns bool)
pub fn save_config_silent(settings: &Settings) -> bool
// Load or create default config
pub fn load_or_create_config() -> Settings
// Utility functions
pub fn get_config_dir() -> Result<PathBuf>
pub fn get_config_file_path() -> Result<PathBuf>
pub fn config_exists() -> bool
pub fn delete_config() -> Result<()>
Error Handling Strategy
The module uses a multi-level approach:
load_config()- Always returns validSettings, falling back to defaults on any errorload_config_strict()- ReturnsResult<Settings>for explicit error handlingsave_config_silent()- Best-effort save for non-critical scenarios (e.g., on exit)
Graceful Fallback Scenarios
| Scenario | Behavior |
|---|---|
| Config file missing | Return defaults |
| Config file empty | Return defaults |
| Invalid JSON | Log warning, return defaults |
| Invalid values | Sanitize to valid ranges |
| Write permission denied | Log error, return false |
Atomic Writes
The save_config() function uses an atomic write pattern:
- Write settings to
config.json.bak - Rename backup to
config.json
This prevents data corruption if the app crashes during write.
Session Restoration (Tab Persistence)
The configuration system includes support for restoring open tabs between sessions.
Tab Info Structure
Each open tab's state is serialized to TabInfo:
pub struct TabInfo {
pub path: Option<PathBuf>, // File path (None for unsaved)
pub modified: bool, // Had unsaved changes
pub cursor_position: (usize, usize), // (line, column)
pub scroll_offset: f32, // Scroll position
}
Session Data in Settings
pub struct Settings {
// ... other fields ...
pub last_open_tabs: Vec<TabInfo>, // Tabs from last session
pub active_tab_index: usize, // Which tab was active
}
Persistence Flow
On Application Exit:
AppState::shutdown()is calledsave_settings()serializes current tabs viaTab::to_tab_info()last_open_tabsandactive_tab_indexare updated in settings- Config is written to disk
On Application Startup:
AppState::new()loads settings viaload_config()restore_session_tabs()is called- For each
TabInfowith a valid path:- If file exists: read content and create
Tab::from_tab_info() - If file missing: log warning and skip
- If path is
None: skip (unsaved tabs not restored)
- If file exists: read content and create
active_tab_indexis restored (clamped to valid range)- If no tabs restored, create an empty tab
Error Handling
| Scenario | Behavior |
|---|---|
| File no longer exists | Skip tab, log warning |
| File read error | Skip tab, log warning |
| Unsaved tab (no path) | Skip tab |
| Invalid active_tab_index | Clamp to valid range |
| All tabs fail to restore | Create empty tab |
Conversion Functions
// Tab to TabInfo (for persistence)
impl Tab {
pub fn to_tab_info(&self) -> TabInfo { ... }
}
// TabInfo to Tab (for restoration)
impl Tab {
pub fn from_tab_info(id: usize, info: &TabInfo, content: String) -> Self { ... }
}
Window State Persistence
The configuration system persists window size, position, and maximize state.
WindowSize Structure
pub struct WindowSize {
pub width: f32,
pub height: f32,
pub x: Option<f32>, // Position (optional)
pub y: Option<f32>,
pub maximized: bool,
}
Dirty Flag Mechanism
Settings are only saved when modified. The AppState tracks changes via a settings_dirty flag:
// Mark settings as needing save
state.mark_settings_dirty();
// Save if dirty (called periodically and on shutdown)
state.save_settings_if_dirty();
Window State Update Flow
update_window_state()is called every frame inapp.rs- Detects size/position changes (threshold: 1.0 pixel)
- Updates
settings.window_sizewith current values - Calls
mark_settings_dirty()to trigger eventual save
Key: The dirty flag must be set when window state changes, otherwise settings won't be persisted (fixed in v0.2.5 - GitHub Issue #15).
Startup Restoration
On startup (main.rs), window state is applied:
// Apply size
viewport.with_inner_size([window_size.width, window_size.height])
// Apply position if saved
if let (Some(x), Some(y)) = (window_size.x, window_size.y) {
viewport.with_position([x, y])
}
// Apply maximized state
if window_size.maximized {
viewport.with_maximized(true)
}
Dependencies Used
dirs(5.x) - Platform-specific directory resolutionserde_json(1.x) - JSON serialization/deserializationlog(0.4) - Logging warnings and errors
Usage
use crate::config::{load_config, save_config, Settings};
// On app startup
let settings = load_config(); // Always succeeds
// After user changes settings
settings.theme = Theme::Dark;
if let Err(e) = save_config(&settings) {
eprintln!("Warning: Could not save settings: {}", e);
}
// On app exit (best effort)
save_config_silent(&settings);
Tests
Run tests with:
cargo test config::persistence
Test coverage includes:
- Platform directory resolution
- Valid/invalid JSON loading
- Partial config (missing fields use defaults)
- Corrupted config fallback
- Save/load roundtrip
- Unknown fields ignored
- Value sanitization
Session restoration tests (in src/state.rs):
test_tab_from_tab_info- TabInfo to Tab conversiontest_restore_session_tabs_empty_settings- No tabs to restoretest_restore_session_tabs_with_missing_file- Graceful handlingtest_restore_session_tabs_skips_unsaved- Unsaved tabs skippedtest_restore_session_tabs_active_index_clamped- Index validationtest_restore_session_tabs_with_temp_file- Full restorationtest_restore_multiple_tabs_with_temp_files- Multiple tabstest_restore_partial_tabs_missing_file- Partial restoration
The module uses tempfile for isolated filesystem tests.