Init
This commit is contained in:
140
Services/AppConfig.cs
Normal file
140
Services/AppConfig.cs
Normal file
@@ -0,0 +1,140 @@
|
||||
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},");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user