diff --git a/CursorLang/Services/SettingsService.cs b/CursorLang/Services/SettingsService.cs
index 1dd0ff0..96d6909 100644
--- a/CursorLang/Services/SettingsService.cs
+++ b/CursorLang/Services/SettingsService.cs
@@ -4,7 +4,9 @@ using System.Text.Json;
using System.Text.Json.Serialization;
using System.Windows.Media;
using System.Windows.Threading;
+using CursorLang.Interop;
using CursorLang.Models;
+using Windows.Storage;
namespace CursorLang.Services;
@@ -31,20 +33,36 @@ public sealed class SettingsService : IDisposable
Converters = { new ColorJsonConverter(), new JsonStringEnumConverter() },
};
+ private const string FileName = "settings.json";
+
private readonly string _filePath;
+ private readonly string _inheritedFilePath;
private readonly DispatcherTimer _saveTimer;
private AppSettings? _settings;
- public SettingsService()
- {
- string folder = Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
- "CursorLang");
- _filePath = Path.Combine(folder, "settings.json");
+ // Sliders change their values continuously, so writing to disk
+ // is postponed until there is a pause in the changes
+ private static readonly TimeSpan SaveDelay = TimeSpan.FromMilliseconds(500);
- // Ползунки меняют значения непрерывно, поэтому запись на диск
- // откладывается до паузы в изменениях
- _saveTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) };
+ public SettingsService()
+ : this(
+ Path.Combine(GetSettingsFolder(), FileName),
+ Path.Combine(GetSeparateInstallFolder(), FileName),
+ SaveDelay)
+ {
+ }
+
+ ///
+ /// Sets the storage locations and the save delay explicitly — thereby making it
+ /// possible to check the work with the file without touching the settings of the
+ /// user themselves.
+ ///
+ internal SettingsService(string filePath, string inheritedFilePath, TimeSpan saveDelay)
+ {
+ _filePath = filePath;
+ _inheritedFilePath = inheritedFilePath;
+
+ _saveTimer = new DispatcherTimer { Interval = saveDelay };
_saveTimer.Tick += OnSaveTimerTick;
}
@@ -54,8 +72,29 @@ public sealed class SettingsService : IDisposable
///
public AppSettings Load()
{
- _settings = ReadFile() ?? CreateDefault();
+ AppSettings? stored = ReadFile(_filePath);
+
+ // There is no file of our own — the application may well have been configured
+ // before the move to a package. Taking the settings from there beats starting
+ // from a blank slate
+ bool inherited = stored is null && _filePath != _inheritedFilePath;
+ if (inherited)
+ {
+ stored = ReadFile(_inheritedFilePath);
+ inherited = stored is not null;
+ }
+
+ _settings = stored ?? CreateDefault();
_settings.PropertyChanged += OnSettingsChanged;
+
+ // Moved settings are fixed in the new place right away rather than on the
+ // first edit: otherwise the application would read someone else's file every
+ // time until then
+ if (inherited)
+ {
+ Save();
+ }
+
return _settings;
}
@@ -89,16 +128,39 @@ public sealed class SettingsService : IDisposable
}
}
- private AppSettings? ReadFile()
+ ///
+ /// The folder the application writes its settings to.
+ ///
+ private static string GetSettingsFolder()
+ {
+ if (!PackageIdentityNative.IsPackaged)
+ {
+ return GetSeparateInstallFolder();
+ }
+
+ // A package has a data folder of its own, which Windows creates and removes
+ // itself. The application name is not appended to it: the folder belongs to it alone anyway
+ return ApplicationData.Current.LocalFolder.Path;
+ }
+
+ ///
+ /// The settings folder of a separately installed application — the same source
+ /// the package inherits the settings from on the first launch.
+ ///
+ private static string GetSeparateInstallFolder() => Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
+ "CursorLang");
+
+ private static AppSettings? ReadFile(string path)
{
try
{
- if (!File.Exists(_filePath))
+ if (!File.Exists(path))
{
return null;
}
- return JsonSerializer.Deserialize(File.ReadAllText(_filePath), SerializerOptions);
+ return JsonSerializer.Deserialize(File.ReadAllText(path), SerializerOptions);
}
catch (Exception e) when (e is IOException or UnauthorizedAccessException or JsonException)
{
diff --git a/CursorLang/ViewModels/SettingsViewModel.cs b/CursorLang/ViewModels/SettingsViewModel.cs
index 70361f7..c19afcc 100644
--- a/CursorLang/ViewModels/SettingsViewModel.cs
+++ b/CursorLang/ViewModels/SettingsViewModel.cs
@@ -64,10 +64,20 @@ public sealed partial class SettingsViewModel : ObservableObject, IDisposable
Color.FromRgb(0xF8, 0x71, 0x71),
];
- public SettingsViewModel(AppSettings settings, ILocalizationService localization)
+ private readonly IStartupService _startup;
+
+ private StartupState _startupState = StartupState.Unavailable;
+
+ public SettingsViewModel(
+ AppSettings settings,
+ ILocalizationService localization,
+ IStartupService startup,
+ UpdateViewModel updates)
{
Settings = settings;
Localization = localization;
+ Updates = updates;
+ _startup = startup;
// The interface language is a setting like any other and is stored in the same place
Localization.CurrentLanguage = settings.Language;
@@ -84,6 +94,9 @@ public sealed partial class SettingsViewModel : ObservableObject, IDisposable
public ILocalizationService Localization { get; }
+ /// The updates section: it has a state and commands of its own.
+ public UpdateViewModel Updates { get; }
+
public IReadOnlyList BackgroundPalette { get; } = Palette;
public IReadOnlyList TextPalette { get; } = ForegroundPalette;
@@ -96,12 +109,64 @@ public sealed partial class SettingsViewModel : ObservableObject, IDisposable
public IReadOnlyList> ScreenPositions { get; }
+ /// Whether to show the startup setting.
+ public bool IsStartupAvailable => _startupState != StartupState.Unavailable;
+
+ /// Startup is up to the application rather than to Windows.
+ public bool CanChangeStartup => _startupState is StartupState.Enabled or StartupState.Disabled;
+
+ /// It has to be explained why the setting does not give in.
+ public bool IsStartupLocked => IsStartupAvailable && !CanChangeStartup;
+
+ ///
+ /// Start the application together with Windows.
+ ///
+ ///
+ /// The value is not stored in the application settings: Windows knows it, and it
+ /// may be changed behind our back — in the "Startup apps" section. So the property
+ /// answers from the last known state of the task every time.
+ ///
+ public bool RunAtStartup
+ {
+ get => _startupState is StartupState.Enabled or StartupState.EnabledByPolicy;
+ set
+ {
+ if (value == RunAtStartup)
+ {
+ return;
+ }
+
+ _ = ApplyStartupAsync(value);
+ }
+ }
+
+ ///
+ /// Asks Windows about the state of startup. Called after the window is shown: the
+ /// answer has to be waited for, while the settings must open right away.
+ ///
+ public async Task InitializeAsync() => UpdateStartupState(await _startup.GetStateAsync());
+
public void Dispose()
{
Settings.PropertyChanged -= OnSettingsChanged;
Localization.PropertyChanged -= OnLocalizationChanged;
}
+ private async Task ApplyStartupAsync(bool enabled) =>
+ UpdateStartupState(await _startup.SetEnabledAsync(enabled));
+
+ private void UpdateStartupState(StartupState state)
+ {
+ _startupState = state;
+
+ // The change is reported even when the state is the same: the checkbox has
+ // already been toggled, and only a re-read value can put it back
+ OnPropertyChanged(nameof(RunAtStartup));
+ OnPropertyChanged(nameof(IsStartupAvailable));
+ OnPropertyChanged(nameof(CanChangeStartup));
+ OnPropertyChanged(nameof(IsStartupLocked));
+ }
+
private void OnSettingsChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(AppSettings.Language))