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.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<AppSettings>(json,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? new();
var json = File.ReadAllText(path);
var loaded = JsonSerializer.Deserialize<AppSettings>(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()
+52 -6
View File
@@ -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<AppSettings>(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<AppSettings>(json,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!;
Assert.Equal("de", back.Language);
Assert.Equal("split", restored.DiffViewMode);
Assert.True(restored.DiffWrapLines);
}
}