Status speech: read line-by-line, MB->GB on totals, double-press to copy (held for next release)

Andre's feedback on the spoken status (the issue #13 feature):
- Speak the status one line at a time instead of collapsing the lines into a run-on sentence —
  this also removes the doubled "." the collapse produced (each line already ended in a period).
- Totals switch to GB once they reach a gigabyte ("4.5 GB" vs "4558.2 MB"), via a FormatDataSize
  helper used by the readout itself so the on-screen and spoken figures stay consistent.
- A quick DOUBLE press of the speak-status hotkey copies the status to the clipboard (with a
  "copied" announcement) so it can be shared with others.

Held for the next release.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ednunp
2026-06-24 14:31:03 +01:00
co-authored by Claude Opus 4.8
parent f9b3ed9e67
commit 0fe9637356
+44 -10
View File
@@ -267,6 +267,8 @@ public sealed class MainForm : Form
// Tracks when the FIRST healthy-peer transition happened in the current "connected" // Tracks when the FIRST healthy-peer transition happened in the current "connected"
// span. Cleared when no peers are healthy. Used for the uptime line. // span. Cleared when no peers are healthy. Used for the uptime line.
private DateTime? statusConnectedSinceUtc; private DateTime? statusConnectedSinceUtc;
// Time of the last "speak status" hotkey press, for double-press-to-copy detection.
private DateTime lastSpeakStatusPressUtc = DateTime.MinValue;
// Per-list state (used by sync helpers — was per-method in the old dialog). // Per-list state (used by sync helpers — was per-method in the old dialog).
private bool suppressConnectedCheck; private bool suppressConnectedCheck;
private bool suppressDiscoveredCheck; private bool suppressDiscoveredCheck;
@@ -3348,18 +3350,40 @@ public sealed class MainForm : Form
statusReadout.Text = text; statusReadout.Text = text;
} }
/// <summary>Speak the current connection status line aloud through the active screen reader, via /// <summary>Speak the current connection status aloud through the active screen reader, via Tolk
/// Tolk (<see cref="ScreenReader"/>). Answers issue #13: NVDA sometimes can't see the status /// (<see cref="ScreenReader"/>). Answers issue #13: NVDA sometimes can't see the status readout
/// readout ("no status line found"), so this reads it on demand. Triggered only by a user-set /// ("no status line found"), so this reads it on demand. Triggered only by a user-set global hotkey
/// global hotkey (screen-reader only, unset by default) — being a system-wide hotkey it works from /// (screen-reader only, unset by default) — being a system-wide hotkey it works from anywhere. The
/// anywhere, whether or not RemSound is focused. Newlines become ". " so the multi-line readout /// status is read line by line (the multi-line text is passed straight to the screen reader, which
/// speaks as a sentence rather than running together.</summary> /// pauses at each line) rather than collapsed into a run-on sentence. A quick DOUBLE press copies
/// the same text to the clipboard instead, so it can be shared. (Andre's suggestion, 2026-06-24.)</summary>
private void SpeakStatusLine() private void SpeakStatusLine()
{ {
var now = DateTime.UtcNow;
var doublePress = now - lastSpeakStatusPressUtc <= TimeSpan.FromMilliseconds(600);
lastSpeakStatusPressUtc = now;
var text = statusReadout.Text; var text = statusReadout.Text;
text = string.IsNullOrWhiteSpace(text) if (string.IsNullOrWhiteSpace(text)) text = "No status information available.";
? "No status information available."
: text.Replace("\r\n", ". ").Replace("\n", ". "); if (doublePress)
{
// Second quick press: copy the status to the clipboard so it can be shared. Clipboard access
// needs the UI thread — this runs on it (the hotkey is marshalled onto the owner form).
try
{
Clipboard.SetText(text);
ScreenReader.Speak("RemSound status copied to the clipboard.");
}
catch (Exception ex)
{
logFile.Event($"speak status: clipboard copy failed: {ex.GetType().Name}: {ex.Message}");
ScreenReader.Speak("Could not copy the status to the clipboard.");
}
return;
}
// Single press: read it out, line by line (the screen reader pauses at each line break).
ScreenReader.Speak(text); ScreenReader.Speak(text);
} }
@@ -3446,10 +3470,20 @@ public sealed class MainForm : Form
} }
sb.AppendLine($"Receiving {rxKbs:0.0} kB/s; sending {txKbs:0.0} kB/s."); sb.AppendLine($"Receiving {rxKbs:0.0} kB/s; sending {txKbs:0.0} kB/s.");
sb.Append($"Total received {receiver.BytesReceived / 1048576.0:0.0} MB; sent {sender.BytesSent / 1048576.0:0.0} MB."); sb.Append($"Total received {FormatDataSize(receiver.BytesReceived)}; sent {FormatDataSize(sender.BytesSent)}.");
return sb.ToString(); return sb.ToString();
} }
/// <summary>A running data total as MB, switching to GB once it reaches a gigabyte — "935.8 MB",
/// "4.5 GB" — so a big figure reads more naturally (Andre's suggestion). Binary units, matching the
/// 1048576-byte MB used elsewhere.</summary>
private static string FormatDataSize(long bytes)
{
const double mb = 1048576.0;
const double gb = 1073741824.0;
return bytes >= gb ? $"{bytes / gb:0.0} GB" : $"{bytes / mb:0.0} MB";
}
private static string FormatUptime(TimeSpan span) private static string FormatUptime(TimeSpan span)
{ {
if (span.TotalSeconds < 1) return "0 seconds"; if (span.TotalSeconds < 1) return "0 seconds";