Files
claude-local-overview-display/Services/AppConfig.cs
2026-06-25 09:59:54 +02:00

141 lines
5.4 KiB
C#

using System.IO;
using System.Text.Json;
using System.Text.RegularExpressions;
namespace ClaudeOverview.Services;
/// <summary>Settings loaded from config.json next to the executable.</summary>
public sealed class AppConfig
{
public string WslDistro { get; set; } = "Ubuntu";
public string LinuxHome { get; set; } = "/home/philipp";
public string ProjectsPathOverride { get; set; } = "";
public int ActiveMinutes { get; set; } = 5;
/// <summary>
/// Fallback window (minutes) for showing "cold" (inactive) chats when the session-limit reset
/// time isn't available. Normally the cold cutoff is the start of the current session-limit
/// window (its reset time minus the 5-hour block); this is only used when that's unknown.
/// </summary>
public int ColdMinutes { get; set; } = 1440;
public int RefreshSeconds { get; set; } = 4;
/// <summary>Seconds a pending tool may sit before we treat it as a permission prompt (vs still running).</summary>
public int PendingPermissionSeconds { get; set; } = 8;
public int MonitorIndex { get; set; } = -1;
public int EdgeMargin { get; set; } = 12;
public bool OnlyVsCode { get; set; } = true;
public bool StartMinimized { get; set; } = true;
public bool FlashOnAttention { get; set; } = true;
public bool SoundOnAttention { get; set; } = false;
public bool ShowSessionLimit { get; set; } = true;
/// <summary>How often to poll the subscription usage endpoint (changes slowly).</summary>
public int UsageRefreshSeconds { get; set; } = 60;
/// <summary>Path to .credentials.json (sibling of the projects dir, i.e. inside .claude).</summary>
public string CredentialsPath
{
get
{
var claudeDir = Path.GetDirectoryName(ProjectsPath);
return claudeDir is null
? ""
: Path.Combine(claudeDir, ".credentials.json");
}
}
/// <summary>Resolves the Windows path to the .claude/projects directory.</summary>
public string ProjectsPath
{
get
{
if (!string.IsNullOrWhiteSpace(ProjectsPathOverride))
return ProjectsPathOverride;
// \\wsl.localhost\<Distro>\<linuxHome>\.claude\projects
var home = LinuxHome.Replace('/', '\\').TrimStart('\\');
return $@"\\wsl.localhost\{WslDistro}\{home}\.claude\projects";
}
}
/// <summary>
/// Translates a Linux path from a transcript (e.g. <c>/home/philipp/git/foo</c>) to the Windows
/// UNC path that reaches it over the WSL share (<c>\\wsl.localhost\Ubuntu\home\philipp\git\foo</c>).
/// </summary>
public string ToWindowsPath(string linuxPath)
{
var rel = linuxPath.Replace('/', '\\').TrimStart('\\');
return $@"\\wsl.localhost\{WslDistro}\{rel}";
}
public static AppConfig Load()
{
try
{
var path = Path.Combine(AppContext.BaseDirectory, "config.json");
if (!File.Exists(path)) return new AppConfig();
var json = File.ReadAllText(path);
var opts = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
ReadCommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true
};
return JsonSerializer.Deserialize<AppConfig>(json, opts) ?? new AppConfig();
}
catch
{
return new AppConfig();
}
}
private static string ConfigFilePath => Path.Combine(AppContext.BaseDirectory, "config.json");
/// <summary>
/// Persists the runtime-editable settings back to config.json, updating values in place
/// so the file's comments and other keys are preserved.
/// </summary>
public void Save()
{
string text = File.Exists(ConfigFilePath)
? File.ReadAllText(ConfigFilePath)
: "{\n}\n";
text = UpsertInt(text, "monitorIndex", MonitorIndex);
text = UpsertInt(text, "activeMinutes", ActiveMinutes);
text = UpsertInt(text, "refreshSeconds", RefreshSeconds);
text = UpsertBool(text, "flashOnAttention", FlashOnAttention);
text = UpsertBool(text, "soundOnAttention", SoundOnAttention);
File.WriteAllText(ConfigFilePath, text);
}
private static string UpsertBool(string json, string key, bool value)
{
var rx = new Regex($"(?<!//\\s)(\"{Regex.Escape(key)}\"\\s*:\\s*)(?:true|false)");
var literal = value ? "true" : "false";
if (rx.IsMatch(json))
return rx.Replace(json, m => m.Groups[1].Value + literal, 1);
int brace = json.IndexOf('{');
if (brace < 0) return json;
return json.Insert(brace + 1, $"\n \"{key}\": {literal},");
}
/// <summary>Replaces the integer value of a JSON key, or inserts the key if missing.</summary>
private static string UpsertInt(string json, string key, int value)
{
// Match the *real* key, not a "// key" comment line: require it isn't preceded by "// ".
var rx = new Regex($"(?<!//\\s)(\"{Regex.Escape(key)}\"\\s*:\\s*)-?\\d+");
if (rx.IsMatch(json))
return rx.Replace(json, m => m.Groups[1].Value + value, 1);
// Not present — insert right after the opening brace.
int brace = json.IndexOf('{');
if (brace < 0) return json;
return json.Insert(brace + 1, $"\n \"{key}\": {value},");
}
}