Files
HellionChat/HellionChat/Util/TabLifecycleHelpers.cs
T
JonKazama-Hellion 6f71b09331 fix(closeout): address closure-review findings
- gate keybind pill-sync on IsChannelOrExistingLinkshell so an empty
  linkshell slot no longer desyncs the pill from the real send channel
- close manually-popped pop-out windows on logout via an IsOpen filter
  instead of the PopOut flag (which manual pops never set)
- read the router's tell-tab lookup through a lock-wrapped accessor so the
  framework thread cannot enumerate Config.Tabs mid worker-thread mutation
- add a "switch on every tell" toggle (default on) and make the auto-open
  mode pick the matching layout, so Sidebar vs Top-tab are distinct
- comment corrections (stale/contradictory text, TEST-MIRROR path depth)
2026-06-16 09:04:18 +02:00

98 lines
4.3 KiB
C#

using HellionChat.Code;
using HellionChat.GameFunctions.Types;
namespace HellionChat.Util;
// Pure predicates for the TempTab pin lifecycle. Extracted from the strip
// sites in Plugin.cs and Configuration.cs so they stay in lockstep — a
// load-time strip that disagrees with the save-time strip is exactly how
// pinned tabs would silently fall out of the JSON.
internal static class TabLifecycleHelpers
{
public static bool IsInUnpinnedPool(Tab t) => t.IsTempTab && !t.IsPinned;
public static bool IsInPinnedPool(Tab t) => t.IsTempTab && t.IsPinned;
public static bool ShouldStripOnLoad(Tab t) => IsInUnpinnedPool(t);
public static bool ShouldStripOnSave(Tab t) => IsInUnpinnedPool(t);
// Stale-tell strip + channel derive, run at every tab activation. When a
// DIFFERENT tab becomes the input surface, drop any runtime tell state the
// game-side detour left on it (the CurrentChannel tell target plus the
// partner-name label) so a normal typed line cannot route as a silent /tell
// to the old partner — the same privacy guard StripTellBindingOnPromote
// applies on promote. Re-activating the already-active tab must NOT strip
// (a live game-tell would lose its context, TR-4); a tab carrying its own
// Tab.TellTarget is a real tell binding (leg1) and is left intact.
internal static void OnTabActivated(Tab tab, Tab? previous)
{
if (
!ReferenceEquals(tab, previous)
&& tab.CurrentChannel.Channel == InputChannel.Tell
&& tab.TellTarget?.IsSet() != true
)
{
tab.CurrentChannel.SetChannel(InputChannel.Invalid);
tab.CurrentChannel.TellTarget = null;
tab.CurrentChannel.ResetTempChannel();
// Label chunks carry the partner name after a game-side tell.
tab.CurrentChannel.Name = [];
}
EnsureCurrentChannel(tab);
}
// Pure derive-helper: resolves a tab's input channel from its
// SelectedChannels when none is set yet. Reached only via OnTabActivated
// now, so the strip and the derive stay in lockstep at every entry.
internal static void EnsureCurrentChannel(Tab tab)
{
if (tab.CurrentChannel.Channel != InputChannel.Invalid)
return;
foreach (var chatType in tab.SelectedChannels.Keys)
{
if (chatType.ToInputChannel() is { } input)
{
tab.CurrentChannel.SetChannel(input);
return;
}
}
}
// Drops a temp/pinned tell tab's binding when it is promoted to a permanent
// tab. Beyond the obvious IsTempTab/IsPinned/Tab.TellTarget reset, this also
// clears the RUNTIME channel's tell state — that part is the CORR-1 guard:
// a spawned tell tab carries CurrentChannel.Channel == Tell plus a resolvable
// CurrentChannel.TellTarget, and neither is touched by clearing Tab.TellTarget
// alone. Without this clear the input bar would route a normal typed line on
// the promoted tab silently as /tell to the OLD partner (a privacy misfire the
// current==Tell routing gate cannot catch, because current here really IS
// Tell). Channel -> Invalid so the next sidebar/top-bar click re-derives the
// channel from SelectedChannels via EnsureCurrentChannel like any normal tab;
// the worst residual is a "/t" with no target, which the game rejects without
// sending (same safe class as the COMP-1 fall-through, no silent send).
internal static void StripTellBindingOnPromote(Tab tab)
{
tab.IsTempTab = false;
tab.IsPinned = false;
tab.TellTarget = TellTarget.Empty();
tab.Channel = null;
tab.CurrentChannel.SetChannel(InputChannel.Invalid);
tab.CurrentChannel.TellTarget = null;
tab.CurrentChannel.ResetTempChannel();
}
// Wrap-around tab index for keybind cycling. Pure so the Build-Suite can test the
// wrap math without a live window. count == 0 returns 0 (the caller dead-zones
// before activating); negative deltas wrap correctly via the double-mod.
// TEST-MIRROR: ../../../Hellion Build test/_Helpers/TabLifecycleHelpersTests.cs
internal static int WrapTabIndex(int current, int delta, int count)
{
if (count <= 0)
return 0;
return ((current + delta) % count + count) % count;
}
}