2026-02-27 07:24:23 -06:00
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text.Json.Serialization ;
using Windows.UI ;
namespace Microsoft.CmdPal.UI.ViewModels.Settings ;
#pragma warning disable SA1402 // File may only contain a single type
/// <summary>
/// Settings for the Dock. These are settings for _the whole dock_. Band-specific
/// settings are in <see cref="DockBandSettings"/>.
/// </summary>
public class DockSettings
{
public DockSide Side { get ; set ; } = DockSide . Top ;
public DockSize DockSize { get ; set ; } = DockSize . Small ;
public DockSize DockIconsSize { get ; set ; } = DockSize . Small ;
// <Theme settings>
public DockBackdrop Backdrop { get ; set ; } = DockBackdrop . Acrylic ;
public UserTheme Theme { get ; set ; } = UserTheme . Default ;
public ColorizationMode ColorizationMode { get ; set ; }
CmdPal: Extract persistence services from SettingsModel and AppStateModel (#46312)
## Summary of the Pull Request
Extracts persistence (load/save) logic from `SettingsModel` and
`AppStateModel` into dedicated service classes, following the
single-responsibility principle. Consumers now interact with
`ISettingsService` and `IAppStateService` instead of receiving raw model
objects through DI.
**New services introduced:**
- `IPersistenceService` / `PersistenceService` — generic `Load<T>` /
`Save<T>` with AOT-compatible `JsonTypeInfo<T>`, ensures target
directory exists before writing
- `ISettingsService` / `SettingsService` — loads settings on
construction, runs migrations, exposes `Settings` property and
`SettingsChanged` event
- `IAppStateService` / `AppStateService` — loads state on construction,
exposes `State` property and `StateChanged` event
**Key changes:**
- `SettingsModel` and `AppStateModel` are now pure data models — all
file I/O, migration, and directory management removed
- Raw `SettingsModel` and `AppStateModel` removed from DI container;
consumers receive the appropriate service
- `IApplicationInfoService.ConfigDirectory` injected into services for
config path resolution (no more hardcoded `Utilities.BaseSettingsPath`)
- ~30 consumer files updated across `Microsoft.CmdPal.UI.ViewModels` and
`Microsoft.CmdPal.UI` projects
- All `#pragma warning disable SA1300` suppressions removed —
convenience accessors replaced with direct `_settingsService.Settings` /
`_appStateService.State` access
- Namespace prefixes (`Services.ISettingsService`) replaced with proper
`using` directives
## PR Checklist
- [ ] **Communication:** I've discussed this with core contributors
already.
- [x] **Tests:** Added/updated and all pass
- [ ] **Localization:** N/A — no end-user-facing strings changed
- [ ] **Dev docs:** N/A — internal refactor, no public API changes
- [ ] **New binaries:** N/A — no new binaries introduced
## Detailed Description of the Pull Request / Additional comments
### Architecture
Services are registered as singletons in `App.xaml.cs`:
```csharp
services.AddSingleton<IPersistenceService, PersistenceService>();
services.AddSingleton<ISettingsService, SettingsService>();
services.AddSingleton<IAppStateService, AppStateService>();
```
`PersistenceService.Save<T>` writes the serialized model directly to
disk, creating the target directory if it doesn't exist. It also does
not attempt to merge existing and new settings/state. `SettingsService`
runs hotkey migrations on load and raises `SettingsChanged` after saves.
`AppStateService` always raises `StateChanged` after saves.
### Files changed (41 files, +1169/−660)
| Area | Files | What changed |
|------|-------|-------------|
| New services | `Services/IPersistenceService.cs`,
`PersistenceService.cs`, `ISettingsService.cs`, `SettingsService.cs`,
`IAppStateService.cs`, `AppStateService.cs` | New service interfaces and
implementations |
| Models | `SettingsModel.cs`, `AppStateModel.cs` | Stripped to pure
data bags |
| DI | `App.xaml.cs` | Service registration, removed raw model DI |
| ViewModels | 12 files | Constructor injection of services |
| UI | 10 files | Service injection replacing model access |
| Settings | `DockSettings.cs` | `Colors.Transparent` replaced with
struct literal to avoid WinUI3 COM dependency |
| Tests | `PersistenceServiceTests.cs`, `SettingsServiceTests.cs`,
`AppStateServiceTests.cs` | 38 unit tests covering all three services |
| Config | `.gitignore` | Added `.squad/`, `.github/agents/` exclusions
|
## Validation Steps Performed
- Built `Microsoft.CmdPal.UI` with MSBuild (x64/Debug) — exit code 0,
clean build
- Ran 38 unit tests via `vstest.console.exe` — all passing
- Verified no remaining `#pragma warning disable SA1300` blocks
- Verified no remaining `Services.` namespace prefixes
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-20 18:58:27 -05:00
public Color CustomThemeColor { get ; set ; } = new ( ) { A = 0 , R = 255 , G = 255 , B = 255 } ; // Transparent — avoids WinUI3 COM dependency on Colors.Transparent and COM in class init
2026-02-27 07:24:23 -06:00
public int CustomThemeColorIntensity { get ; set ; } = 100 ;
public int BackgroundImageOpacity { get ; set ; } = 20 ;
public int BackgroundImageBlurAmount { get ; set ; }
public int BackgroundImageBrightness { get ; set ; }
public BackgroundImageFit BackgroundImageFit { get ; set ; }
public string? BackgroundImagePath { get ; set ; }
// </Theme settings>
// public List<string> PinnedCommands { get; set; } = [];
public List < DockBandSettings > StartBands { get ; set ; } = [ ] ;
public List < DockBandSettings > CenterBands { get ; set ; } = [ ] ;
public List < DockBandSettings > EndBands { get ; set ; } = [ ] ;
public bool ShowLabels { get ; set ; } = true ;
[JsonIgnore]
public IEnumerable < ( string ProviderId , string CommandId ) > AllPinnedCommands = >
StartBands . Select ( b = > ( b . ProviderId , b . CommandId ) )
. Concat ( CenterBands . Select ( b = > ( b . ProviderId , b . CommandId ) ) )
. Concat ( EndBands . Select ( b = > ( b . ProviderId , b . CommandId ) ) ) ;
public DockSettings ( )
{
// Initialize with default values
// PinnedCommands = [
// "com.microsoft.cmdpal.winget"
// ];
StartBands . Add ( new DockBandSettings
{
ProviderId = "com.microsoft.cmdpal.builtin.core" ,
CommandId = "com.microsoft.cmdpal.home" ,
} ) ;
StartBands . Add ( new DockBandSettings
{
ProviderId = "WinGet" ,
CommandId = "com.microsoft.cmdpal.winget" ,
ShowLabels = false ,
} ) ;
EndBands . Add ( new DockBandSettings
{
ProviderId = "PerformanceMonitor" ,
CommandId = "com.microsoft.cmdpal.performanceWidget" ,
} ) ;
EndBands . Add ( new DockBandSettings
{
ProviderId = "com.microsoft.cmdpal.builtin.datetime" ,
CommandId = "com.microsoft.cmdpal.timedate.dockBand" ,
} ) ;
}
}
/// <summary>
/// Settings for a specific dock band. These are per-band settings stored
/// within the overall <see cref="DockSettings"/>.
/// </summary>
public class DockBandSettings
{
public required string ProviderId { get ; set ; }
public required string CommandId { get ; set ; }
/// <summary>
/// Gets or sets whether titles are shown for items in this band.
/// If null, falls back to dock-wide ShowLabels setting.
/// </summary>
public bool? ShowTitles { get ; set ; }
/// <summary>
/// Gets or sets whether subtitles are shown for items in this band.
/// If null, falls back to dock-wide ShowLabels setting.
/// </summary>
public bool? ShowSubtitles { get ; set ; }
/// <summary>
/// Gets or sets a value for backward compatibility. Maps to ShowTitles.
/// </summary>
[System.Text.Json.Serialization.JsonIgnore]
public bool? ShowLabels
{
get = > ShowTitles ;
set = > ShowTitles = value ;
}
/// <summary>
/// Resolves the effective value of <see cref="ShowTitles"/> for this band.
/// If this band doesn't have a specific value set, we'll fall back to the
/// dock-wide setting (passed as <paramref name="defaultValue"/>).
/// </summary>
public bool ResolveShowTitles ( bool defaultValue ) = > ShowTitles ? ? defaultValue ;
/// <summary>
/// Resolves the effective value of <see cref="ShowSubtitles"/> for this band.
/// If this band doesn't have a specific value set, we'll fall back to the
/// dock-wide setting (passed as <paramref name="defaultValue"/>).
/// </summary>
public bool ResolveShowSubtitles ( bool defaultValue ) = > ShowSubtitles ? ? defaultValue ;
public DockBandSettings Clone ( )
{
return new ( )
{
ProviderId = this . ProviderId ,
CommandId = this . CommandId ,
ShowTitles = this . ShowTitles ,
ShowSubtitles = this . ShowSubtitles ,
} ;
}
}
public enum DockSide
{
Left = 0 ,
Top = 1 ,
Right = 2 ,
Bottom = 3 ,
}
public enum DockSize
{
Small ,
Medium ,
Large ,
}
public enum DockBackdrop
{
Transparent ,
Acrylic ,
}
#pragma warning restore SA1402 // File may only contain a single type