Release v4.3: speak status line (Tolk) + Logging tab with housekeeping

- New screen-reader hotkey "Speak the RemSound status information" (issue #13):
  reads the status line aloud through the active screen reader via Tolk, fires from
  anywhere (system-wide), unset by default. Built behind an IScreenReaderOutput seam
  so a future build can swap Tolk for Prism on Windows 10+ without touching callers.
  Tolk DLLs vendored under tolk/ and shipped next to the exe.
- New Logging tab in Preferences: Enable logs + Write logs now moved there, plus
  opt-in startup "warn if logs folder exceeds N MB" and "delete logs older than N days",
  and a "Delete all logs" button (Yes/No confirm). New LogMaintenance helper + AppConfig
  settings drive it.
- Manual (readme.html + regenerated MANUAL.md), About changelog and RELEASE_NOTES
  updated in plain English; csproj <Version> bumped to 4.3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ednunp
2026-06-19 13:09:06 +01:00
co-authored by Claude Opus 4.8
parent 034a7de632
commit fd5c31740a
20 changed files with 783 additions and 26 deletions
+85
View File
@@ -0,0 +1,85 @@
using RemSound.Core;
namespace RemSound.App;
/// <summary>
/// Housekeeping for the <c>logs\</c> folder, driven by the Logging-tab preferences: report the
/// folder's total size (for the startup over-size warning), prune log files older than N days, and
/// delete every log on demand. All operations are best-effort — a locked or vanished file is
/// skipped, never thrown, because log housekeeping must never stop the app from running.
/// </summary>
internal static class LogMaintenance
{
/// <summary>Total size in bytes of every <c>*.log</c> file in the logs folder. 0 if the folder
/// doesn't exist yet or can't be read.</summary>
public static long LogsFolderSizeBytes()
{
try
{
var dir = AppConfig.LogsDirectory;
if (!Directory.Exists(dir)) return 0;
long total = 0;
foreach (var file in Directory.EnumerateFiles(dir, "*.log"))
{
try { total += new FileInfo(file).Length; } catch { /* vanished mid-scan — skip */ }
}
return total;
}
catch { return 0; }
}
/// <summary>Delete <c>*.log</c> files last written more than <paramref name="days"/> days ago.
/// The currently-open log (<paramref name="activeLogPath"/>) is always spared even if the clock
/// makes it look old. Returns how many files were deleted.</summary>
public static int PruneLogsOlderThan(int days, string? activeLogPath)
{
if (days < 1) return 0;
var deleted = 0;
try
{
var dir = AppConfig.LogsDirectory;
if (!Directory.Exists(dir)) return 0;
var cutoff = DateTime.Now.AddDays(-days);
foreach (var file in Directory.EnumerateFiles(dir, "*.log"))
{
if (IsSamePath(file, activeLogPath)) continue;
try
{
if (File.GetLastWriteTime(file) < cutoff)
{
File.Delete(file);
deleted++;
}
}
catch { /* locked / in use — leave it */ }
}
}
catch { /* folder enumeration failed — give up quietly */ }
return deleted;
}
/// <summary>Delete every <c>*.log</c> file except the one currently being written
/// (<paramref name="activeLogPath"/>, which is held open and can't be removed anyway). Returns
/// how many files were deleted.</summary>
public static int DeleteAllLogs(string? activeLogPath)
{
var deleted = 0;
try
{
var dir = AppConfig.LogsDirectory;
if (!Directory.Exists(dir)) return 0;
foreach (var file in Directory.EnumerateFiles(dir, "*.log"))
{
if (IsSamePath(file, activeLogPath)) continue;
try { File.Delete(file); deleted++; }
catch { /* locked / in use — leave it */ }
}
}
catch { /* give up quietly */ }
return deleted;
}
private static bool IsSamePath(string a, string? b) =>
!string.IsNullOrEmpty(b)
&& string.Equals(Path.GetFullPath(a), Path.GetFullPath(b), StringComparison.OrdinalIgnoreCase);
}