using System.IO;
using System.Text.Json;
using System.Text.RegularExpressions;
namespace ClaudeOverview.Services;
/// Settings loaded from config.json next to the executable.
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;
///
/// 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.
///
public int ColdMinutes { get; set; } = 1440;
public int RefreshSeconds { get; set; } = 4;
/// Seconds a pending tool may sit before we treat it as a permission prompt (vs still running).
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;
/// How often to poll the subscription usage endpoint (changes slowly).
public int UsageRefreshSeconds { get; set; } = 60;
/// Path to .credentials.json (sibling of the projects dir, i.e. inside .claude).
public string CredentialsPath
{
get
{
var claudeDir = Path.GetDirectoryName(ProjectsPath);
return claudeDir is null
? ""
: Path.Combine(claudeDir, ".credentials.json");
}
}
/// Resolves the Windows path to the .claude/projects directory.
public string ProjectsPath
{
get
{
if (!string.IsNullOrWhiteSpace(ProjectsPathOverride))
return ProjectsPathOverride;
// \\wsl.localhost\\\.claude\projects
var home = LinuxHome.Replace('/', '\\').TrimStart('\\');
return $@"\\wsl.localhost\{WslDistro}\{home}\.claude\projects";
}
}
///
/// Translates a Linux path from a transcript (e.g. /home/philipp/git/foo) to the Windows
/// UNC path that reaches it over the WSL share (\\wsl.localhost\Ubuntu\home\philipp\git\foo).
///
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(json, opts) ?? new AppConfig();
}
catch
{
return new AppConfig();
}
}
private static string ConfigFilePath => Path.Combine(AppContext.BaseDirectory, "config.json");
///
/// Persists the runtime-editable settings back to config.json, updating values in place
/// so the file's comments and other keys are preserved.
///
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($"(? m.Groups[1].Value + literal, 1);
int brace = json.IndexOf('{');
if (brace < 0) return json;
return json.Insert(brace + 1, $"\n \"{key}\": {literal},");
}
/// Replaces the integer value of a JSON key, or inserts the key if missing.
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($"(? 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},");
}
}