Add per-profile "Lock to exact peer addresses" setting (#17, held for next release)

Feature request from the same singer: a way to pin a profile to exact peer addresses
so RemSound never matches the other computer by its advertised name and never switches
to a different address it discovers — for a machine reachable at two addresses at once
(VPN + LAN), they want only the one they chose, and would rather the connection die than
wander. The logical end of the v4.7/v4.8 direction.

New per-profile Profile.LockPeerAddresses (default false), round-tripped through
RemSoundSettingsStore (ApplyProfile/CopyTo + Load/SaveLockPeerAddresses), mirroring the
PriorityMode pattern. New AccessibleCheckBox on the Connectivity tab ("Lock to these exact
peer addresses, no matter what — never follow names or switch", Alt+L), saved with the
profile, marks the profile dirty on change. When set, RefreshKnownPeers early-returns
before the discovered-peer merge and the address-follow, so the profile's peers stay
exactly as set (the allow-list is still pushed). Off = unchanged behaviour.

Manual: rewrote the "Connecting to one specific IP" section (which over-promised that
add-by-IP "can never drift" — the merge/follow could) into an unambiguous "Locking a
profile to one exact address" section, plus a Connectivity-tab control-table row.

Held for the next release.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ednunp
2026-06-28 15:31:50 +01:00
co-authored by Claude Opus 4.8
parent 0df2a576fc
commit c7d422e3ba
5 changed files with 86 additions and 17 deletions
+33 -5
View File
@@ -238,6 +238,14 @@ public sealed class MainForm : Form
private readonly CheckedListBox rememberedPeersList = new() { CheckOnClick = true, Width = 430, Height = 90, AccessibleName = "Remembered peers (Alt+R)" };
private readonly Label rememberedPeersStatus = new() { AutoSize = true, Text = "No remembered peer selected." };
private readonly Button manualAddButton = new() { Text = "Add peer by IP (Alt+&A)", AutoSize = true, AccessibleName = "Add peer by IP" };
// Per-profile "pin to exact addresses" toggle (#17). When ticked, RefreshKnownPeers skips the
// discovered-peer merge and the address-follow, so the profile's peers stay exactly as the user set.
private readonly AccessibleCheckBox lockPeerAddressesBox = new()
{
Text = "Lock to these exact peer addresses, no matter what — never follow names or switch (Alt+&L)",
AccessibleName = "Lock to these exact peer addresses, no matter what; never follow names or switch address",
AutoSize = true,
};
// loggingBox + writeLogsNowButton field instances retired 2026-05-08 — both controls
// now live inside PreferencesDialog. The form-level logFile.Enabled gate is set
// directly from the settings store at startup (see ApplyLoggingEnabled).
@@ -2797,7 +2805,7 @@ public sealed class MainForm : Form
Dock = DockStyle.Fill,
Padding = new Padding(12),
ColumnCount = 2,
RowCount = 5,
RowCount = 6,
AutoScroll = true,
};
panel.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
@@ -2921,19 +2929,29 @@ public sealed class MainForm : Form
// Preferences dialog (File → Preferences, Ctrl+P) as the last two items.
// === Layout ===
// 5 rows: 02 the three peer lists, 3 manual-add, 4 connection-status readout.
panel.RowCount = 5;
// 6 rows: 02 the three peer lists, 3 manual-add, 4 the lock-to-fixed-addresses toggle, 5 status.
panel.RowCount = 6;
FormLayoutRows.AddCheckedListRow(panel, 0, "Connected peers (Alt+&C)", connectedPeersList, connectedPeersStatus, FocusListControl);
FormLayoutRows.AddCheckedListRow(panel, 1, "Discovered peers (Alt+&D)", discoveredPeersList, discoveredPeersStatus, FocusListControl);
FormLayoutRows.AddCheckedListRow(panel, 2, "Remembered peers (Alt+&R)", rememberedPeersList, rememberedPeersStatus, FocusListControl);
panel.Controls.Add(new Label { Text = "Manual peer", AutoSize = true, Anchor = AnchorStyles.Left }, 0, 3);
panel.Controls.Add(manualAddButton, 1, 3);
lockPeerAddressesBox.Checked = settings.LoadLockPeerAddresses();
lockPeerAddressesBox.CheckedChanged += (_, _) =>
{
settings.SaveLockPeerAddresses(lockPeerAddressesBox.Checked);
MarkProfileDirty();
logFile.Event($"lock peer addresses: {(lockPeerAddressesBox.Checked ? "on" : "off")}");
};
panel.Controls.Add(lockPeerAddressesBox, 0, 4);
panel.SetColumnSpan(lockPeerAddressesBox, 2);
// Connection status readout — last row, tab-into-able.
var statusLabel = new MnemonicLabel { Text = "Connection status (Alt+&S)", AutoSize = true, Anchor = AnchorStyles.Left, MnemonicTarget = statusReadout };
statusLabel.Click += (_, _) => statusReadout.Focus();
panel.Controls.Add(statusLabel, 0, 4);
panel.Controls.Add(statusReadout, 1, 4);
panel.Controls.Add(statusLabel, 0, 5);
panel.Controls.Add(statusReadout, 1, 5);
// Initial render so the box has content the moment the user tabs into it.
RefreshStatusReadout();
@@ -5226,6 +5244,16 @@ public sealed class MainForm : Form
foreach (var peer in discovery.Peers) knownPeers[peer.InstanceId] = peer;
foreach (var peer in manualPeers.Values) knownPeers[peer.InstanceId] = peer;
// Locked-to-fixed-addresses profiles (#17): the user wants RemSound to use exactly the peer
// addresses they set and never substitute one found on the network. Skip the discovered-peer
// merge (which would attach a computer name to their selection) and the address-follow below;
// the selection's allow-list is still pushed so audio flows to those exact addresses, unchanged.
if (settings.LoadLockPeerAddresses())
{
PushAllowedReceiveSenders();
return;
}
// Dedupe by endpoint (address:port). When a manual peer (typed by IP) and a discovered
// peer (broadcasting hostname) point to the same machine, drop the manual entry and
// forward any active selection to the discovered peer so the user doesn't lose it.
+6
View File
@@ -177,6 +177,12 @@ public sealed class Profile
/// list at save time. On load, RemSound auto-connects to any of these that resolve.</summary>
public List<string> SelectedConnectedPeers { get; set; } = [];
/// <summary>When true, this profile is LOCKED to the exact peer addresses set above. RemSound uses
/// only those addresses, never matches the other computer by the name it advertises on the network,
/// and never switches to a different address it discovers — even if the set address stops working
/// (the connection simply waits or drops rather than moving). Off by default. (#17)</summary>
public bool LockPeerAddresses { get; set; }
// === Hotkeys ===
public HotkeyRecord? ReceiveMuteHotkey { get; set; }
public HotkeyRecord? SendMuteHotkey { get; set; }
@@ -282,6 +282,19 @@ public sealed class RemSoundSettingsStore
Save(s);
}
/// <summary>When true, this profile is locked to the exact peer addresses the user set — RemSound
/// uses only those, never follows the other computer by its advertised name and never switches to a
/// discovered address. Off by default; opt in per profile. (#17)</summary>
public bool LoadLockPeerAddresses(bool defaultValue = false) =>
Try(() => Load()?.LockPeerAddresses) ?? defaultValue;
public void SaveLockPeerAddresses(bool value)
{
var s = Load() ?? new Settings();
s.LockPeerAddresses = value;
Save(s);
}
/// <summary>Suppresses the connect/disconnect sound cues that play when a peer's health
/// transitions to/from Healthy. Off by default — cues are on. Saved per-profile so users
/// who don't want them in a given setup don't have to remember to mute every session.
@@ -537,6 +550,7 @@ public sealed class RemSoundSettingsStore
SendRate = profile.SendRate,
TightLatencyMode = profile.TightLatencyMode,
PriorityMode = profile.PriorityMode,
LockPeerAddresses = profile.LockPeerAddresses,
Smoothness = profile.Smoothness,
ConcealmentArtifact = (ConcealmentArtifact)profile.ConcealmentArtifactRaw,
MuteConnectionCues = profile.MuteConnectionCues,
@@ -581,6 +595,7 @@ public sealed class RemSoundSettingsStore
if (s.SendRate is SendRate sr) profile.SendRate = sr;
if (s.TightLatencyMode is bool tl) profile.TightLatencyMode = tl;
if (s.PriorityMode is bool pm) profile.PriorityMode = pm;
if (s.LockPeerAddresses is bool lpa) profile.LockPeerAddresses = lpa;
if (s.Smoothness is int sm) profile.Smoothness = sm;
if (s.ConcealmentArtifact is ConcealmentArtifact ca) profile.ConcealmentArtifactRaw = (int)ca;
if (s.MuteConnectionCues is bool mc) profile.MuteConnectionCues = mc;
@@ -626,6 +641,7 @@ public sealed class RemSoundSettingsStore
public SendRate? SendRate { get; set; }
public bool? TightLatencyMode { get; set; }
public bool? PriorityMode { get; set; }
public bool? LockPeerAddresses { get; set; }
public int? Smoothness { get; set; }
public ConcealmentArtifact? ConcealmentArtifact { get; set; }
public bool? MuteConnectionCues { get; set; }