Settings Modified Indicator Design
Problem Statement
The current settings UI has several UX issues related to the "modified" indicator:
Incorrect baseline for comparison: The current implementation compares settings values against the schema default. This causes:
- Auto-discovered content (like plugins) to show as "modified" on initial load because they differ from the empty schema default
{} - The "Reset" button clears plugins entirely because it resets to schema default
- Auto-discovered content (like plugins) to show as "modified" on initial load because they differ from the empty schema default
Section-level indicators are misleading: The dot indicators on sections (General, Editor, Plugins) show "modified" based on comparison to schema defaults, not based on what the user has actually configured in the current layer.
No visibility into individual item modifications: Users cannot see which specific items have been modified at the current layer vs inherited from a lower layer.
Hidden inheritance sources: Users have no transparency into where a setting's effective value originates, making it difficult to understand configuration state.
Goals
Design a UX similar to IntelliJ IDEA's and VS Code's settings:
- Show which items are explicitly defined in the current target layer (User/Project)
- "Reset" should remove the value from the current layer, falling back to inherited
- Auto-managed content (plugins) should not show as "modified" and should not be resettable
- Provide transparency into inheritance hierarchy ("Effective Value" visualization)
Layered Configuration Architecture
Fresh uses a 4-layer configuration system (highest precedence first):
- Session - Temporary runtime overrides (not persisted)
- Project - Project-specific settings (
.fresh/config.json) - User - User-global settings (
~/.config/fresh/config.json) - System - Built-in defaults (schema defaults)
Values cascade: higher layers override lower layers. The final config is the merge of all layers.
Inheritance Evaluation Model
The effective value V_eff of a setting s is determined by evaluating layers in precedence order:
V_eff(s) = V(s, layer_k) where k = max{i | V(s, layer_i) is defined}In other words: the effective value comes from the highest-precedence layer that defines it.
Design
Transparency of Inheritance (Critical)
Research indicates that systems hiding the source of a setting's value cause the highest degree of user frustration. The UI must clearly show:
- Where each value originates (layer source badge)
- What would happen if the value were reset (inheritance fallback)
- Which items are user-configured vs inherited
This follows the "Recognizability over Recall" principle - users should see inheritance state at a glance rather than having to remember or investigate.
Definition of "Modified"
Current behavior: modified = (current_value != schema_default)
Proposed behavior: modified = (value is defined in target_layer)
For example, when editing User layer settings:
- An item is "modified" if it has a value defined in the User layer
- An item is NOT modified if it comes from System defaults or is undefined
This aligns with the UX concept: "modified" means "the user explicitly configured this in the current layer."
Section-Level Indicators
The dot indicator next to category names (e.g., "General", "Editor") should show:
- Filled dot (●): At least one item in this section is defined in the target layer
- Empty (space): No items in this section are defined in the target layer
This is computed by aggregating: category_modified = any(item.modified for item in category.items)
Individual Item Indicators
Each setting item should display:
Layer source badge: A small label showing which layer the current value comes from
- Positioned inline after the setting name or value
- Color-coded by layer:
[default]- dimmed/gray (System layer)[user]- subtle highlight (User layer)[project]- distinct color (Project layer)[session]- ephemeral indicator (Session layer)
Modified indicator (●): Shows if the item is defined in the current target layer
- Appears as a small dot next to the setting name
- Only visible when the setting is defined in the target layer being edited
Visual Hierarchy
Following the research on visual hierarchy, the modified indicator should have higher visual weight than the layer source badge, as it represents actionable state (something the user can reset).
┌─────────────────────────────────────────────────────────────┐
│ ● Tab Size : [ 4 ] [-] [+] [project] │
│ Number of spaces per tab │
├─────────────────────────────────────────────────────────────┤
│ Line Numbers : [x] [default] │
│ Show line numbers in the gutter │
└─────────────────────────────────────────────────────────────┘In this example:
- "Tab Size" has ● because it's defined in the current target layer (Project)
- "Line Numbers" has no ● because its value comes from defaults
Reset Behavior
Current behavior: Reset sets the value to schema default.
Proposed behavior: Reset removes the value from the current layer's delta.
This means:
- If User layer defines
tab_size: 2, clicking Reset removes it from User layer - The value then falls back to System default (or Project layer if editing Session)
- Items not defined in the current layer have nothing to reset
- Reset button should be disabled/hidden for items not defined in current layer
Reset Confirmation
For destructive operations (reset all in section, reset to defaults), show a confirmation indicating:
- What values will be removed
- What the new effective values will be after reset
Auto-Managed Content (Maps with x-no-add)
Plugins and other auto-discovered content use x-no-add schema extension:
- These Maps are populated automatically, not by user configuration
- They should never show as "modified" (even though they differ from empty default)
- They should never be resettable (Reset has no meaning for auto-discovered content)
- They should skip the modified calculation entirely
- Individual entries within these maps CAN show modified (e.g., disabling a plugin)
Progressive Disclosure
Following the 80/20 principle from UX research:
- Show commonly-used settings by default
- Hide advanced settings behind expandable sections
- Consider adding "Show Advanced" toggle per category
Currently Fresh shows all settings flat, which may be acceptable given the search functionality.
Implementation Changes
1. build_item / build_page functions
Add parameters:
layer_sources: &HashMap<String, ConfigLayer>- Maps paths to their source layertarget_layer: ConfigLayer- The layer being edited
Calculate modified as:
// For regular items
let modified = layer_sources.get(&schema.path) == Some(&target_layer);
// For Maps with no_add (auto-managed)
let modified = false; // Container is never "modified"
// For entries WITHIN Maps (even no_add maps)
// Check if the specific entry path exists in target layer
let entry_path = format!("{}/{}", schema.path, entry_key);
let entry_modified = layer_sources.get(&entry_path) == Some(&target_layer);2. reset_current_to_default function
Change from:
// Set value to schema default
self.set_pending_change(&path, default.clone());To:
// Remove value from delta (fall back to inherited)
// Only if the item is defined in the current layer
if item.modified {
self.remove_from_delta(&path);
// Recalculate effective value from remaining layers
let new_value = self.compute_effective_value(&path);
self.update_control(&path, new_value);
}3. Add remove_from_delta method
New method to remove a path from the pending changes that will result in deletion from the layer:
/// Mark a path for removal from the current layer's delta.
/// On save, this path will be deleted from the layer file.
pub fn remove_from_delta(&mut self, path: &str) {
// Use a special marker value or separate tracking for deletions
self.pending_deletions.insert(path.to_string());
self.pending_changes.remove(path);
}4. Section indicator calculation
Already correct: page.items.iter().any(|i| i.modified)
Once modified is calculated correctly per-item, section indicators will automatically work.
5. Render layer source badges
In render.rs, add rendering for layer source badges:
fn render_layer_badge(layer: ConfigLayer, theme: &Theme) -> Span {
let (text, style) = match layer {
ConfigLayer::System => ("default", Style::default().fg(theme.text_muted)),
ConfigLayer::User => ("user", Style::default().fg(theme.text_secondary)),
ConfigLayer::Project => ("project", Style::default().fg(theme.accent)),
ConfigLayer::Session => ("session", Style::default().fg(theme.warning)),
};
Span::styled(format!("[{}]", text), style)
}Migration Path
- Update
build_itemsignature to accept layer info - Update all callers (
build_page,build_pages) - Pass
layer_sourcesandtarget_layerfromSettingsState - Add
pending_deletionstracking toSettingsState - Update
reset_current_to_defaultto remove from delta - Add layer source badge rendering
- Update tests that rely on old "modified" semantics
Testing Considerations
- Modified indicator accuracy: Test that items defined in target layer show ●
- Reset behavior: Test that reset removes from delta, not sets to default
- Auto-managed content: Test that
no_addmaps don't show modified - Layer switching: Test that modified indicators update when switching target layer
- Save/Load cycle: Test that saved changes persist correctly and show as modified on reload
Future Considerations
- Effective Value Visualization: On hover/focus, show a "Policy Stack" popup displaying all layers that attempted to define the value (similar to AWS IAM Policy Simulator)
- Diff view: Side-by-side comparison showing what's defined at each layer
- Search by layer: Filter settings to show only those defined in a specific layer
- Conflict indicators: When a higher layer overrides a lower layer, consider visual indication
- Export/Import: Allow exporting layer-specific settings for sharing
- Validation warnings: Show warnings when settings might conflict or cause issues
Alignment with Industry Best Practices
This design follows key principles identified in configuration UX research:
- Transparency of Inheritance: Clear layer source badges and modified indicators
- Safety Through Fallback: Reset removes from layer rather than clearing entirely
- Good Defaults: Auto-managed content doesn't pollute modified state
- Explicit Save Model: Changes are staged, not auto-applied (matches VS Code/IntelliJ)
- Search First: Existing search functionality supports navigation (keep this)
- Visual Hierarchy: Modified indicator has higher weight than source badge