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},"); } }