Skip to content

feat: split settings into global (user prefs) and local (per-folder)#122

Open
blksmr wants to merge 4 commits intoerictli:mainfrom
blksmr:feat/global-settings
Open

feat: split settings into global (user prefs) and local (per-folder)#122
blksmr wants to merge 4 commits intoerictli:mainfrom
blksmr:feat/global-settings

Conversation

@blksmr
Copy link
Copy Markdown
Contributor

@blksmr blksmr commented Mar 27, 2026

Closes #121

Problem

All settings were stored in {NOTES_FOLDER}/.scratch/settings.json, meaning user preferences (theme, typography, zoom, etc.) reset every time a different notes folder was selected.

Solution

Split settings into two buckets:

Global — ~/.config/scratch/settings.json

Shared across all notes folders, persists regardless of which folder is active and survives app uninstall/reinstall:

  • Theme (mode + custom colors)
  • Editor font (family, size, bold weight, line height)
  • Text direction
  • Editor width / custom width px
  • Interface zoom
  • Ollama model
  • Folders enabled (tree view preference)

Stored in ~/.config/scratch/ instead of the Tauri app data directory so settings are not lost when the app is uninstalled and reinstalled.

Local — {NOTES_FOLDER}/.scratch/settings.json

Specific to the active notes folder:

  • Git enabled
  • Pinned note IDs
  • Default note name template

Implementation

  • Added GlobalSettings and LocalSettings structs in Rust
  • AppState now holds global_settings and local_settings separately
  • get_settings() returns a merged view — no frontend API change
  • update_settings() transparently splits writes to the correct file
  • Global settings stored in ~/.config/scratch/settings.json (cross-platform, persists across reinstalls)
  • Automatic migration from old Tauri app data location on first launch
  • Local settings reload on folder switch

Test plan

  • Change theme/typography → switch notes folder → settings persist
  • Pin a note in folder A → switch to folder B → pins are folder-specific
  • Enable git on folder A → switch to folder B → git state is folder-specific
  • Verify ~/.config/scratch/settings.json is created on first settings change
  • Verify .scratch/settings.json only contains local fields
  • Uninstall and reinstall app → global settings are preserved
  • Existing users: old settings in app data dir are migrated automatically

Summary by CodeRabbit

  • New Features

    • Settings split into Global (app-wide preferences) and Local (per-folder: git, pinned notes, default note name); frontend still shows a single combined settings view.
    • One-time migration imports legacy settings into the new global location when needed.
  • Chores

    • Global settings load at startup; local settings load when a notes folder is active. Global changes persist always; local changes persist only when a folder is set.
  • Bug Fixes

    • Note/folder operations update and persist local settings; local persistence failures are logged instead of surfacing errors.

Global settings (user preferences) are now persisted in
{APP_CONFIG_DIR}/settings.json and shared across all notes folders:
theme, editor font, text direction, editor width, interface zoom,
ollama model, and folders enabled.

Local settings (folder-specific) remain in .scratch/settings.json:
git enabled, pinned note IDs, and default note name template.

The frontend API contract (Settings type) is unchanged — get_settings
returns a merged view and update_settings splits writes to the correct
file transparently.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 27, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 099bc0ba-3562-4f13-a154-4d7542131286

📥 Commits

Reviewing files that changed from the base of the PR and between 11c6490 and 309ef59.

📒 Files selected for processing (2)
  • src-tauri/src/lib.rs
  • src/types/note.ts
✅ Files skipped from review due to trivial changes (1)
  • src-tauri/src/lib.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/types/note.ts

📝 Walkthrough

Walkthrough

Split persisted settings into GlobalSettings (OS config dir) and LocalSettings (per-notes-folder). AppState, startup/migration, load/save helpers, and Tauri commands were updated to load, compose, and persist these separately while preserving the frontend Settings shape.

Changes

Cohort / File(s) Summary
Backend: settings storage & state
src-tauri/src/lib.rs
Replaced RwLock<Settings> with RwLock<GlobalSettings> and RwLock<LocalSettings> in AppState. Added GlobalSettings/LocalSettings types, global/local load & save helpers, and one-time migration from legacy Tauri app-data to OS config dir. Startup now unconditionally loads global settings and conditionally loads local settings when notes_folder is set.
Backend: command & mutation flows
src-tauri/src/lib.rs
Updated get_settings to compose frontend Settings from global + local. update_settings persists global always, local only when notes_folder active. Git-enabled, pinned-note-id, folder rename/move, and note-move flows now mutate/persist local_settings via save_local_settings; local save errors are logged rather than propagated. Discovery/search-index and ignore-pattern reads now use GlobalSettings.
Frontend types
src/types/note.ts
Split exported types into GlobalSettings (theme/editor/text/interface/ollama/folders/ignored-patterns/custom colors) and LocalSettings (gitEnabled, pinnedNoteIds, defaultNoteName). Settings re-exported as extends GlobalSettings, LocalSettings to keep the combined API shape.

Sequence Diagram

sequenceDiagram
    actor Client
    participant App as AppState
    participant GlobalStore as Global\nSettings File
    participant LocalStore as Local\nSettings File

    rect rgba(150,200,100,0.5)
    Note over Client,App: Settings load & compose flow
    Client->>App: request settings (for folder)
    App->>GlobalStore: load {app_config_dir}/settings.json
    GlobalStore-->>App: global settings
    App->>LocalStore: load {notes_folder}/.scratch/settings.json (if folder set)
    LocalStore-->>App: local settings
    App->>App: compose Settings (global + local)
    App-->>Client: Settings
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 I hopped through code with tidy paws,

Split the stash without a pause.
Global roots and local nests,
Settings safe in separate rests.
A little hop, a wiser cause. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 69.23% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: splitting settings into global and local buckets, which is the primary objective of this PR.
Linked Issues check ✅ Passed The PR successfully implements the core requirement from #121: moving user preferences to a global config location (~/.config/scratch/settings.json) that persists across folder switches, while keeping folder-specific fields locally.
Out of Scope Changes check ✅ Passed All changes are directly related to the linked issue #121. The split of settings into global and local stores, migration logic, and updates to persistence layers are all within scope of the stated objective.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
src-tauri/src/lib.rs (1)

760-788: Consider async I/O for settings persistence.

save_global_settings and save_local_settings use synchronous std::fs::write. While the JSON payloads are small, this blocks the executor thread. Per coding guidelines, all I/O should be non-blocking async operations. On slow or network-mounted storage, this could cause brief UI freezes.

If the impact is deemed acceptable for these small writes, consider adding a brief comment explaining the trade-off. Otherwise, wrapping in tokio::task::spawn_blocking would comply with the guideline.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src-tauri/src/lib.rs` around lines 760 - 788, The current blocking file I/O
in save_global_settings, save_local_settings and load_local_settings should be
made non-blocking: convert these functions to async (save_global_settings ->
async fn save_global_settings(...)->Result<()>, save_local_settings similarly,
and load_local_settings -> async fn returning LocalSettings) and perform disk
access inside tokio::task::spawn_blocking closures (or use
tokio::fs::write/read_to_string equivalents) so the executor thread isn't
blocked; update all callers to await the new async functions and preserve
serde_json::to_string_pretty usage (or run that CPU work inside the
spawn_blocking if desired). Ensure error handling and Result types remain
consistent after the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src-tauri/src/lib.rs`:
- Around line 1690-1693: The call to save_local_settings(&folder, &local) is
silently discarding its Result with "let _ =" like the earlier rename_folder and
move_note cases; change this to handle errors explicitly by either propagating
the Result (use the ? operator where the caller returns Result) or by logging
the error (e.g., with tracing::error!/log::error!/eprintln!) so failures are
visible; update the call site that contains save_local_settings to remove the
silent discard and add appropriate error handling/logging consistent with how
rename_folder and move_note were fixed.
- Around line 1495-1498: The call to save_local_settings currently discards its
Result with `let _ = save_local_settings(&folder, &local);`, risking silent
failure and data loss for updated pinnedNoteIds; change this to handle the
Result instead—either propagate the error from the enclosing function (return
the Result from the function and use `?` on save_local_settings) or at minimum
log the error using your logger (e.g., `if let Err(e) =
save_local_settings(&folder, &local) { log::error!(...) }`), referencing
save_local_settings, folder, local and the pinnedNoteIds update so failures are
surfaced to the caller or recorded.
- Around line 1589-1592: The call to save_local_settings is currently discarding
errors with let _ = save_local_settings(&folder, &local);—instead either
propagate the Result or log the error like you did in rename_folder; replace the
discard with proper handling (e.g., use save_local_settings(&folder, &local)? to
propagate from the enclosing function or match/save_local_settings(..) { Ok(_)
=> {}, Err(e) => tracing::error!("saving local settings failed: {:?}", e) } ) so
failures aren't silently ignored.

---

Nitpick comments:
In `@src-tauri/src/lib.rs`:
- Around line 760-788: The current blocking file I/O in save_global_settings,
save_local_settings and load_local_settings should be made non-blocking: convert
these functions to async (save_global_settings -> async fn
save_global_settings(...)->Result<()>, save_local_settings similarly, and
load_local_settings -> async fn returning LocalSettings) and perform disk access
inside tokio::task::spawn_blocking closures (or use
tokio::fs::write/read_to_string equivalents) so the executor thread isn't
blocked; update all callers to await the new async functions and preserve
serde_json::to_string_pretty usage (or run that CPU work inside the
spawn_blocking if desired). Ensure error handling and Result types remain
consistent after the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ab39df62-83c4-4c62-841b-b75a8d9175d2

📥 Commits

Reviewing files that changed from the base of the PR and between 5178073 and f6e31b7.

📒 Files selected for processing (2)
  • src-tauri/src/lib.rs
  • src/types/note.ts

Comment thread src-tauri/src/lib.rs
Comment thread src-tauri/src/lib.rs
Comment thread src-tauri/src/lib.rs
blksmr added 2 commits March 27, 2026 19:37
… dir

Global settings now persist across app uninstall/reinstall by using
~/.config/scratch/settings.json instead of the Tauri app data directory.
Includes automatic migration from the old location on first launch.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src-tauri/src/lib.rs (2)

697-710: Cross-platform path for global settings deviates from coding guideline.

The implementation stores global settings at ~/.config/scratch/settings.json instead of {APP_DATA}/config.json as specified in the coding guidelines. Per the PR commit message, this is intentional to persist settings across app uninstall/reinstall.

Note: ~/.config/ is not the standard config location on macOS (~/Library/Application Support/ or ~/Library/Preferences/), but it works cross-platform and is commonly used by CLI tools and cross-platform apps. If macOS conventions become important later, consider using dirs::config_dir() from the dirs crate which returns platform-appropriate paths.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src-tauri/src/lib.rs` around lines 697 - 710, The current
get_global_settings_dir and get_global_settings_path hardcode ~/.config/scratch
which deviates from the platform-specific guideline; update
get_global_settings_dir() to use dirs::config_dir() (from the dirs crate) to
obtain the platform-appropriate config directory (falling back to the existing
HOME/USERPROFILE logic if config_dir() returns None), then join "scratch" and
create the directory; ensure get_global_settings_path() still returns that dir
joined with "settings.json" and add the dirs crate to Cargo.toml.

776-800: Migration preserves old settings file.

The migration copies the old settings to the new location but doesn't remove the old file at {APP_DATA}/settings.json. This is likely intentional for safety (rollback capability), but may cause confusion if users manually edit the old location expecting changes to take effect. Consider adding a comment explaining this design choice.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src-tauri/src/lib.rs` around lines 776 - 800, The migrate_global_settings
function copies settings from the old Tauri app_data location (using
app.path().app_data_dir()) to the new path returned by
get_global_settings_path() but deliberately leaves the old file in place; add a
brief comment in migrate_global_settings explaining that the old file is
intentionally preserved for rollback/compatibility reasons (and note any future
plan to remove it or how users should manage it) so maintainers and reviewers
understand the design choice around not deleting old_path after writing
new_path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src-tauri/src/lib.rs`:
- Around line 697-710: The current get_global_settings_dir and
get_global_settings_path hardcode ~/.config/scratch which deviates from the
platform-specific guideline; update get_global_settings_dir() to use
dirs::config_dir() (from the dirs crate) to obtain the platform-appropriate
config directory (falling back to the existing HOME/USERPROFILE logic if
config_dir() returns None), then join "scratch" and create the directory; ensure
get_global_settings_path() still returns that dir joined with "settings.json"
and add the dirs crate to Cargo.toml.
- Around line 776-800: The migrate_global_settings function copies settings from
the old Tauri app_data location (using app.path().app_data_dir()) to the new
path returned by get_global_settings_path() but deliberately leaves the old file
in place; add a brief comment in migrate_global_settings explaining that the old
file is intentionally preserved for rollback/compatibility reasons (and note any
future plan to remove it or how users should manage it) so maintainers and
reviewers understand the design choice around not deleting old_path after
writing new_path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2697cca6-5487-48ec-aca5-c37bbc78c1b0

📥 Commits

Reviewing files that changed from the base of the PR and between bb88e2c and 11c6490.

📒 Files selected for processing (1)
  • src-tauri/src/lib.rs

@erictli
Copy link
Copy Markdown
Owner

erictli commented Apr 3, 2026

I'm going to leave this for a subsequent (maybe the next) release. I want to do this along with a change to make it so that you can open multiple instances of Scratch pointed at different folders.

Resolve conflict in src/types/note.ts by placing the new
ignoredPatterns, customColorsLight and customColorsDark fields in
GlobalSettings (user prefs shared across folders) instead of the
unified Settings struct from main.

In src-tauri/src/lib.rs, extend GlobalSettings with the same three
fields, update get_settings/update_settings to route them through
global, change get_effective_ignored_dirs to take &GlobalSettings, and
rewrite all state.settings read/write sites to use state.global_settings.
Copilot AI review requested due to automatic review settings April 16, 2026 18:50
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Splits application settings into global (user preferences) and local (per-notes-folder) buckets while keeping the frontend API shape unchanged by returning a merged Settings view.

Changes:

  • Added GlobalSettings and LocalSettings models and updated AppState to store them separately.
  • Implemented global settings load/save + local settings load/save, and updated settings consumers to read from the correct bucket.
  • Updated get_settings()/update_settings() to merge/split settings transparently.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
src/types/note.ts Splits the TS settings types into GlobalSettings/LocalSettings and keeps Settings as the combined API contract.
src-tauri/src/lib.rs Introduces Rust GlobalSettings/LocalSettings, updates state + commands to merge/split settings, and adds global settings persistence/migration logic.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src-tauri/src/lib.rs
}

// Per-folder settings (stored in .scratch/settings.json within notes folder)
// Global settings – stored in {APP_CONFIG_DIR}/settings.json
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doc comment says global settings are stored in {APP_CONFIG_DIR}/settings.json, but get_global_settings_path() below actually targets ~/.config/scratch/settings.json. Please update the comment to match the real path/behavior (or describe it as the platform config directory).

Suggested change
// Global settings – stored in {APP_CONFIG_DIR}/settings.json
// Global settings – stored in the platform-specific app config directory
// (for example, ~/.config/scratch/settings.json on Linux).

Copilot uses AI. Check for mistakes.
Comment thread src-tauri/src/lib.rs
Comment on lines +738 to +748
// Get global settings directory (~/.config/scratch/), survives app uninstall/reinstall
fn get_global_settings_dir() -> Result<PathBuf> {
let home = std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.map_err(|_| anyhow::anyhow!("Cannot determine home directory"))?;
let dir = PathBuf::from(home).join(".config").join("scratch");
std::fs::create_dir_all(&dir)?;
Ok(dir)
}

// Get global settings file path (~/.config/scratch/settings.json, shared across all folders)
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_global_settings_dir() hardcodes ~/.config/scratch for all OSes and derives “home” from HOME/USERPROFILE. This won’t follow platform conventions (XDG XDG_CONFIG_HOME on Linux, ~/Library/Application Support on macOS, %AppData% on Windows) and contradicts the PR’s “cross-platform” claim. Consider using a standard config-dir resolver (e.g., directories/dirs crate, or Tauri’s path APIs) and then appending scratch/settings.json.

Suggested change
// Get global settings directory (~/.config/scratch/), survives app uninstall/reinstall
fn get_global_settings_dir() -> Result<PathBuf> {
let home = std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.map_err(|_| anyhow::anyhow!("Cannot determine home directory"))?;
let dir = PathBuf::from(home).join(".config").join("scratch");
std::fs::create_dir_all(&dir)?;
Ok(dir)
}
// Get global settings file path (~/.config/scratch/settings.json, shared across all folders)
// Get global settings directory in the OS-specific user config location, survives app uninstall/reinstall
fn get_global_settings_dir() -> Result<PathBuf> {
let config_dir =
dirs::config_dir().ok_or_else(|| anyhow::anyhow!("Cannot determine config directory"))?;
let dir = config_dir.join("scratch");
std::fs::create_dir_all(&dir)?;
Ok(dir)
}
// Get global settings file path (scratch/settings.json in the OS-specific config directory, shared across all folders)

Copilot uses AI. Check for mistakes.
Comment thread src-tauri/src/lib.rs
let home = std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.map_err(|_| anyhow::anyhow!("Cannot determine home directory"))?;
let dir = PathBuf::from(home).join(".config").join("scratch");
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_global_settings_dir() creates the directory as a side effect (create_dir_all) and it’s called during startup via load_global_settings(). That means ~/.config/scratch/ will be created on app launch even if the user never changes settings, which is a behavior change and conflicts with the test plan note (“created on first settings change”). Consider deferring directory creation to the save path only, or updating the documented behavior.

Suggested change
let dir = PathBuf::from(home).join(".config").join("scratch");
Ok(PathBuf::from(home).join(".config").join("scratch"))
}
// Ensure the global settings directory exists for write operations.
fn ensure_global_settings_dir() -> Result<PathBuf> {
let dir = get_global_settings_dir()?;

Copilot uses AI. Check for mistakes.
Comment thread src-tauri/src/lib.rs
Comment on lines +829 to +833
// Check old location (Tauri app_data_dir)
let old_path = match app.path().app_data_dir() {
Ok(dir) => dir.join("settings.json"),
Err(_) => return,
};
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

migrate_global_settings() only checks for a legacy settings.json in Tauri’s app_data_dir(). However, before this PR settings were stored in {NOTES_FOLDER}/.scratch/settings.json, so existing users’ global prefs (theme/editor font/zoom/etc.) won’t be migrated to the new global file and will reset to defaults. Consider migrating by reading the existing per-folder settings file as the old Settings, splitting into GlobalSettings + LocalSettings, writing the new global file, and rewriting the local file to contain only local fields.

Copilot uses AI. Check for mistakes.
Comment thread src-tauri/src/lib.rs
Comment on lines +754 to 757
fn get_local_settings_path(notes_folder: &str) -> PathBuf {
let scratch_dir = PathBuf::from(notes_folder).join(".scratch");
std::fs::create_dir_all(&scratch_dir).ok();
scratch_dir.join("settings.json")
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_local_settings_path() ignores errors from create_dir_all. In failure cases (read-only notes folder, permission issues, etc.) load_local_settings() will silently fall back to defaults and save_local_settings() may later fail with a less-informative error. Returning a Result<PathBuf> here and propagating directory creation errors would make failures explicit and easier to debug.

Suggested change
fn get_local_settings_path(notes_folder: &str) -> PathBuf {
let scratch_dir = PathBuf::from(notes_folder).join(".scratch");
std::fs::create_dir_all(&scratch_dir).ok();
scratch_dir.join("settings.json")
fn get_local_settings_path(notes_folder: &str) -> Result<PathBuf> {
let scratch_dir = PathBuf::from(notes_folder).join(".scratch");
std::fs::create_dir_all(&scratch_dir)?;
Ok(scratch_dir.join("settings.json"))

Copilot uses AI. Check for mistakes.
Comment thread src/types/note.ts

// Per-folder settings (stored in .scratch/settings.json)
export interface Settings {
// Global settings – shared across all notes folders ({APP_CONFIG_DIR}/settings.json)
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says global settings live in {APP_CONFIG_DIR}/settings.json, but the backend implementation writes to ~/.config/scratch/settings.json (see src-tauri/src/lib.rs). Please update this comment to reflect the actual location (or describe it generically as the OS config directory) so the type docs don’t mislead consumers.

Suggested change
// Global settings – shared across all notes folders ({APP_CONFIG_DIR}/settings.json)
// Global settings – shared across all notes folders (stored in the app's OS config directory, e.g. settings.json)

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Settings should be global (~/.scratch/settings.json) instead of per-folder

3 participants