Files
RemSound/src/RemSound.App/TolkScreenReaderOutput.cs
T
EdnunpandClaude Opus 4.8 fd5c31740a 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>
2026-06-19 13:09:06 +01:00

71 lines
2.7 KiB
C#

using System.Runtime.InteropServices;
namespace RemSound.App;
/// <summary>
/// Tolk-backed <see cref="IScreenReaderOutput"/>. Tolk (https://github.com/dkager/tolk, LGPL-3.0) is a
/// screen-reader abstraction DLL that auto-detects whichever reader is running (NVDA, JAWS, Window-Eyes,
/// SuperNova, System Access, ZoomText) and falls back to SAPI, then routes speech to it. The native
/// <c>Tolk.dll</c> plus its helpers (<c>nvdaControllerClient64.dll</c>, <c>SAAPI64.dll</c>) ship flat
/// next to the exe (see the .csproj Content items) — Tolk.dll loads those helpers from the same folder.
///
/// Loaded lazily on the first <see cref="Speak"/> and never re-attempted once it fails, so a missing DLL
/// or absent screen reader just means silence, never an exception. All entry points are wrapped in
/// try/catch and guarded by a lock, so it's safe to call from any thread (e.g. a hotkey callback).
/// </summary>
internal sealed class TolkScreenReaderOutput : IScreenReaderOutput
{
private readonly object sync = new();
private bool loadAttempted;
private bool loaded;
public bool Speak(string text, bool interrupt = true)
{
if (string.IsNullOrWhiteSpace(text)) return false;
if (!EnsureLoaded()) return false;
try { return Tolk_Output(text, interrupt); }
catch { return false; }
}
public void Shutdown()
{
lock (sync)
{
if (!loaded) return;
try { Tolk_Unload(); } catch { /* best-effort */ }
loaded = false;
loadAttempted = false;
}
}
/// <summary>Load Tolk once. Succeeds only if Tolk loads AND reports a working speech channel, so a
/// machine with the DLLs present but no screen reader running stays silent instead of half-init'd.</summary>
private bool EnsureLoaded()
{
lock (sync)
{
if (loaded) return true;
if (loadAttempted) return false; // already tried and failed — don't spam load attempts
loadAttempted = true;
try { loaded = Tolk_Load() && Tolk_HasSpeech(); }
catch { loaded = false; }
return loaded;
}
}
[DllImport("Tolk.dll", CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool Tolk_Load();
[DllImport("Tolk.dll")]
private static extern void Tolk_Unload();
[DllImport("Tolk.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool Tolk_HasSpeech();
[DllImport("Tolk.dll", CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool Tolk_Output(string text, [MarshalAs(UnmanagedType.Bool)] bool interrupt);
}