feat(diff): persist diff view mode and wrap preference in ui config

This commit is contained in:
mika kuns
2026-08-07 09:22:16 +02:00
parent ebbee6005d
commit 59827757f0
2 changed files with 76 additions and 13 deletions
+24 -7
View File
@@ -1,4 +1,5 @@
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization;
using ClaudeDo.Data; using ClaudeDo.Data;
namespace ClaudeDo.Ui; namespace ClaudeDo.Ui;
@@ -10,24 +11,40 @@ public sealed class AppSettings
public string Language { get; set; } = ""; public string Language { get; set; } = "";
public string AccentPreset { 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 try
{ {
if (File.Exists(ConfigPath)) if (File.Exists(path))
{ {
var json = File.ReadAllText(ConfigPath); var json = File.ReadAllText(path);
return JsonSerializer.Deserialize<AppSettings>(json, var loaded = JsonSerializer.Deserialize<AppSettings>(json,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? new(); new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (loaded is not null)
{
loaded.ConfigPath = path;
return loaded;
}
} }
} }
catch catch
{ {
// Fall through to defaults // Fall through to defaults
} }
return new(); return new AppSettings { ConfigPath = path };
} }
public void Save() public void Save()
+52 -6
View File
@@ -1,22 +1,68 @@
using System.Text.Json; using System.Text.Json;
using ClaudeDo.Ui; using ClaudeDo.Ui;
using Xunit;
namespace ClaudeDo.Ui.Tests; namespace ClaudeDo.Ui.Tests;
public class AppSettingsTests public class AppSettingsTests
{ {
private static string TempConfigPath() =>
Path.Combine(Path.GetTempPath(), $"claudedo-uicfg-{Guid.NewGuid():N}.json");
[Fact] [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] [Fact]
public void Language_round_trips_through_json() public void DiffPreferences_SurviveSaveAndLoad()
{ {
var json = JsonSerializer.Serialize(new AppSettings { Language = "de" }); var path = TempConfigPath();
var back = JsonSerializer.Deserialize<AppSettings>(json, 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<AppSettings>(json,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!; new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!;
Assert.Equal("de", back.Language);
Assert.Equal("split", restored.DiffViewMode);
Assert.True(restored.DiffWrapLines);
} }
} }