diff --git a/src/ClaudeDo.Ui/AppSettings.cs b/src/ClaudeDo.Ui/AppSettings.cs index 09a82aff..fad7fe9d 100644 --- a/src/ClaudeDo.Ui/AppSettings.cs +++ b/src/ClaudeDo.Ui/AppSettings.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using System.Text.Json.Serialization; using ClaudeDo.Data; namespace ClaudeDo.Ui; @@ -10,24 +11,40 @@ public sealed class AppSettings public string Language { get; set; } = ""; public string AccentPreset { get; set; } = ""; - private static readonly string ConfigPath = Paths.Expand("~/.todo-app/ui.config.json"); + /// Diff viewer layout: "unified" or "split". A view preference, so it lives here in + /// ui.config.json rather than in the worker-owned AppSettingsEntity. + public string DiffViewMode { get; set; } = "unified"; + public bool DiffWrapLines { get; set; } - public static AppSettings Load() + private static readonly string DefaultConfigPath = Paths.Expand("~/.todo-app/ui.config.json"); + + /// Where this instance persists. Instance-level (not static) so tests can redirect it — + /// the diff-viewer toggles call Save() on every flip. + [JsonIgnore] + public string ConfigPath { get; set; } = DefaultConfigPath; + + public static AppSettings Load(string? configPath = null) { + var path = configPath ?? DefaultConfigPath; try { - if (File.Exists(ConfigPath)) + if (File.Exists(path)) { - var json = File.ReadAllText(ConfigPath); - return JsonSerializer.Deserialize(json, - new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? new(); + var json = File.ReadAllText(path); + var loaded = JsonSerializer.Deserialize(json, + new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + if (loaded is not null) + { + loaded.ConfigPath = path; + return loaded; + } } } catch { // Fall through to defaults } - return new(); + return new AppSettings { ConfigPath = path }; } public void Save() diff --git a/tests/ClaudeDo.Ui.Tests/AppSettingsTests.cs b/tests/ClaudeDo.Ui.Tests/AppSettingsTests.cs index 88ffdd10..ea22d434 100644 --- a/tests/ClaudeDo.Ui.Tests/AppSettingsTests.cs +++ b/tests/ClaudeDo.Ui.Tests/AppSettingsTests.cs @@ -1,22 +1,68 @@ using System.Text.Json; using ClaudeDo.Ui; +using Xunit; namespace ClaudeDo.Ui.Tests; public class AppSettingsTests { + private static string TempConfigPath() => + Path.Combine(Path.GetTempPath(), $"claudedo-uicfg-{Guid.NewGuid():N}.json"); + [Fact] - public void Language_defaults_to_empty() + public void DiffPreferences_DefaultToUnifiedAndNoWrap() { - Assert.Equal("", new AppSettings().Language); + var settings = new AppSettings(); + + Assert.Equal("unified", settings.DiffViewMode); + Assert.False(settings.DiffWrapLines); } [Fact] - public void Language_round_trips_through_json() + public void DiffPreferences_SurviveSaveAndLoad() { - var json = JsonSerializer.Serialize(new AppSettings { Language = "de" }); - var back = JsonSerializer.Deserialize(json, + var path = TempConfigPath(); + try + { + new AppSettings { ConfigPath = path, DiffViewMode = "split", DiffWrapLines = true }.Save(); + + var restored = AppSettings.Load(path); + + Assert.Equal("split", restored.DiffViewMode); + Assert.True(restored.DiffWrapLines); + Assert.Equal(path, restored.ConfigPath); + } + finally + { + if (File.Exists(path)) File.Delete(path); + } + } + + [Fact] + public void ConfigPath_IsNotWrittenIntoTheConfigFile() + { + var path = TempConfigPath(); + try + { + new AppSettings { ConfigPath = path }.Save(); + + Assert.DoesNotContain("ConfigPath", File.ReadAllText(path), StringComparison.OrdinalIgnoreCase); + } + finally + { + if (File.Exists(path)) File.Delete(path); + } + } + + [Fact] + public void DiffPreferences_ReadFromCamelCasedConfig() + { + const string json = """{"diffViewMode":"split","diffWrapLines":true}"""; + + var restored = JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!; - Assert.Equal("de", back.Language); + + Assert.Equal("split", restored.DiffViewMode); + Assert.True(restored.DiffWrapLines); } }