diff --git a/src/SwitchifyPc.App/App.xaml.cs b/src/SwitchifyPc.App/App.xaml.cs
index 5dd7331..5726dd1 100644
--- a/src/SwitchifyPc.App/App.xaml.cs
+++ b/src/SwitchifyPc.App/App.xaml.cs
@@ -48,6 +48,7 @@ public partial class App : System.Windows.Application
private BluetoothRemoteFrameProcessor? bluetoothFrameProcessor;
private DesktopCommandExecutor? commandExecutor;
private JsonSwitchControlProfileStore? switchControlProfileStore;
+ private SwitchControlSessionManager? switchControlSessionManager;
private MouseRepeatController? mouseRepeatController;
private WindowsCursorOverlayNotifier? cursorOverlay;
private WindowsModifierKeyOverlayNotifier? modifierOverlay;
@@ -143,6 +144,7 @@ protected override void OnExit(ExitEventArgs e)
mouseRepeatController = null;
try
{
+ switchControlSessionManager?.StopAllAsync().GetAwaiter().GetResult();
commandExecutor?.ReleaseHeldInputsAsync().GetAwaiter().GetResult();
}
catch
@@ -157,6 +159,7 @@ protected override void OnExit(ExitEventArgs e)
themeManager?.Dispose();
themeManager = null;
commandExecutor = null;
+ switchControlSessionManager = null;
switchControlProfileStore = null;
bluetoothFrameProcessor = null;
bluetoothStatusTracker = null;
@@ -297,7 +300,7 @@ private void ShowSwitchControlProfiles()
{
switchControlProfileWindow = new SwitchControlProfileWindow(
switchControlProfileStore,
- () => null);
+ () => switchControlSessionManager?.ActiveProfileId);
switchControlProfileWindow.Closed += (_, _) => switchControlProfileWindow = null;
}
switchControlProfileWindow.Owner = settingsWindow;
@@ -384,6 +387,9 @@ private async Task StartBluetoothAsync()
JsonCursorOverlaySettingsStore cursorOverlaySettingsStore = new(Path.Combine(userDataDirectory, "cursor-overlay-settings.json"));
SendInputWindowsNativeInput nativeInput = new();
WindowsDesktopInputAdapter inputAdapter = new(nativeInput, pointerSettingsStore.Load());
+ WindowsGridSwitchBroadcaster gridSwitchBroadcaster = new();
+ switchControlProfileStore = new JsonSwitchControlProfileStore(
+ Path.Combine(userDataDirectory, "switch-control-profiles.json"));
cursorOverlay = new WindowsCursorOverlayNotifier(
nativeInput,
cursorOverlaySettingsStore,
@@ -397,7 +403,12 @@ private async Task StartBluetoothAsync()
inputAdapter,
cursorOverlay,
modifierOverlay: modifierOverlay,
- gridSwitchBroadcaster: new WindowsGridSwitchBroadcaster());
+ gridSwitchBroadcaster: gridSwitchBroadcaster);
+ switchControlSessionManager = new SwitchControlSessionManager(
+ switchControlProfileStore,
+ new SwitchOutputSessionFactory(inputAdapter, gridSwitchBroadcaster));
+ switchControlSessionManager.ActiveProfileChanged += profileName =>
+ Dispatcher.BeginInvoke(() => mainWindowViewModel.SetActiveSwitchControlProfile(profileName));
mouseRepeatController = new MouseRepeatController(
commandExecutor,
mouseRepeatSettingsStore,
@@ -408,7 +419,8 @@ private async Task StartBluetoothAsync()
commandExecutor,
new WindowsPointerProfileProvider(nativeInput, pointerSettingsStore, mouseRepeatSettingsStore),
mouseRepeatController,
- pointerSpeedController);
+ pointerSpeedController,
+ switchControlSessionManager);
pairingApprovalManager ??= new PairingApprovalManager(pairingStore);
RemoteControlSession remoteSession = new(
@@ -465,6 +477,11 @@ private void HandleBluetoothEvent(BluetoothHelperEvent helperEvent)
bluetoothFrameProcessor?.RemoveConnection(disconnected.ConnectionId);
if (status.ConnectedClientCount == 0)
{
+ if (switchControlSessionManager is not null)
+ {
+ await switchControlSessionManager.StopAllAsync();
+ }
+
if (mouseRepeatController is not null)
{
await mouseRepeatController.StopAllAsync();
diff --git a/src/SwitchifyPc.App/MainWindow.xaml b/src/SwitchifyPc.App/MainWindow.xaml
index a5d4c88..63f5be0 100644
--- a/src/SwitchifyPc.App/MainWindow.xaml
+++ b/src/SwitchifyPc.App/MainWindow.xaml
@@ -265,6 +265,12 @@
FontWeight="Bold" />
+
-
+ Height="720"
+ ResizeMode="NoResize"
+ WindowStartupLocation="CenterOwner"
+ Icon="Assets/icon.ico"
+ FontFamily="Segoe UI"
+ Background="{DynamicResource ChromeBackground}"
+ Foreground="{DynamicResource Text}"
+ WindowStyle="None">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+ Foreground="{DynamicResource ChromeSubtleForeground}"
+ Style="{StaticResource MutedText}" />
-
+
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/src/SwitchifyPc.Core/Control/ControlSession.cs b/src/SwitchifyPc.Core/Control/ControlSession.cs
index a23b265..d4df06b 100644
--- a/src/SwitchifyPc.Core/Control/ControlSession.cs
+++ b/src/SwitchifyPc.Core/Control/ControlSession.cs
@@ -3,6 +3,7 @@
using SwitchifyPc.Core.Input;
using SwitchifyPc.Core.Pairing;
using SwitchifyPc.Core.Settings;
+using SwitchifyPc.Core.SwitchControl;
using SwitchifyPc.Protocol;
namespace SwitchifyPc.Core.Control;
@@ -46,19 +47,22 @@ public sealed class ControlSession
private readonly IPointerProfileProvider pointerProfileProvider;
private readonly MouseRepeatController? mouseRepeatController;
private readonly PointerSpeedController? pointerSpeedController;
+ private readonly SwitchControlSessionManager? switchControlSessions;
public ControlSession(
CommandAuthValidator authValidator,
DesktopCommandExecutor commandExecutor,
IPointerProfileProvider pointerProfileProvider,
MouseRepeatController? mouseRepeatController = null,
- PointerSpeedController? pointerSpeedController = null)
+ PointerSpeedController? pointerSpeedController = null,
+ SwitchControlSessionManager? switchControlSessions = null)
{
this.authValidator = authValidator;
this.commandExecutor = commandExecutor;
this.pointerProfileProvider = pointerProfileProvider;
this.mouseRepeatController = mouseRepeatController;
this.pointerSpeedController = pointerSpeedController;
+ this.switchControlSessions = switchControlSessions;
}
public async Task ProcessMessageAsync(string rawMessage, CancellationToken cancellationToken = default)
@@ -91,6 +95,10 @@ public async Task ProcessMessageAsync(string rawMessage, C
}
await commandExecutor.ReleaseHeldInputsAsync(cancellationToken).ConfigureAwait(false);
+ if (failedDeviceId is not null && switchControlSessions is not null)
+ {
+ await switchControlSessions.StopForDeviceAsync(failedDeviceId, cancellationToken).ConfigureAwait(false);
+ }
commandExecutor.EndControlSession();
return ControlSessionResult.Response(ErrorResponse(RequestIdOrNull(request), auth.Reason ?? "invalid_auth", "Command authentication failed."))
with { AuthFailureReason = auth.Reason ?? "invalid_auth" };
@@ -100,6 +108,10 @@ public async Task ProcessMessageAsync(string rawMessage, C
{
await StopRepeatAsync(auth.DeviceId ?? "").ConfigureAwait(false);
await commandExecutor.ReleaseHeldInputsAsync(cancellationToken).ConfigureAwait(false);
+ if (switchControlSessions is not null)
+ {
+ await switchControlSessions.StopForDeviceAsync(auth.DeviceId ?? "", cancellationToken).ConfigureAwait(false);
+ }
commandExecutor.EndControlSession();
return WithAuth(AckOrNoResponse(request), auth);
}
@@ -111,8 +123,109 @@ public async Task ProcessMessageAsync(string rawMessage, C
auth);
}
+ if (type == ProtocolConstants.SwitchProfileListCommandType)
+ {
+ if (switchControlSessions is null)
+ {
+ return WithAuth(ControlSessionResult.Response(ErrorResponse(
+ RequestIdOrNull(request),
+ "unsupported_command",
+ "PC Switch Control is unavailable.")), auth);
+ }
+
+ return WithAuth(ResponseOrNoResponse(
+ request,
+ SwitchProfileCatalogResponse(
+ request.GetProperty("id").GetString() ?? "",
+ switchControlSessions.GetCatalog())), auth);
+ }
+
CommandExecutionResult result;
- if (type == "mouse.repeat.start")
+ if (type == ProtocolConstants.SwitchSessionStartCommandType)
+ {
+ if (switchControlSessions is null)
+ {
+ result = CommandExecutionResult.Failure("unsupported_command", "PC Switch Control is unavailable.");
+ }
+ else
+ {
+ await StopRepeatAsync(auth.DeviceId ?? "").ConfigureAwait(false);
+ await commandExecutor.ReleaseHeldInputsAsync(cancellationToken).ConfigureAwait(false);
+ JsonElement payload = request.GetProperty("payload");
+ SwitchSessionResult sessionResult = await switchControlSessions.StartAsync(
+ auth.DeviceId ?? "",
+ payload.GetProperty("sessionId").GetString() ?? "",
+ payload.GetProperty("profileId").GetString() ?? "",
+ payload.GetProperty("profileVersion").GetInt32(),
+ cancellationToken).ConfigureAwait(false);
+ result = SessionResult(sessionResult);
+ }
+ }
+ else if (type == ProtocolConstants.SwitchEdgeCommandType)
+ {
+ JsonElement payload = request.GetProperty("payload");
+ result = switchControlSessions is null
+ ? CommandExecutionResult.Failure("unsupported_command", "PC Switch Control is unavailable.")
+ : SessionResult(await switchControlSessions.ApplyEdgeAsync(
+ auth.DeviceId ?? "",
+ payload.GetProperty("sessionId").GetString() ?? "",
+ payload.GetProperty("sequence").GetInt64(),
+ payload.GetProperty("switchId").GetInt32(),
+ payload.GetProperty("state").GetString() == "down",
+ cancellationToken).ConfigureAwait(false));
+ }
+ else if (type == ProtocolConstants.SwitchSyncCommandType)
+ {
+ JsonElement payload = request.GetProperty("payload");
+ result = switchControlSessions is null
+ ? CommandExecutionResult.Failure("unsupported_command", "PC Switch Control is unavailable.")
+ : SessionResult(await switchControlSessions.SynchronizeAsync(
+ auth.DeviceId ?? "",
+ payload.GetProperty("sessionId").GetString() ?? "",
+ payload.GetProperty("sequence").GetInt64(),
+ payload.GetProperty("pressedSwitchIds").EnumerateArray().Select(value => value.GetInt32()).ToHashSet(),
+ cancellationToken).ConfigureAwait(false));
+ }
+ else if (type == ProtocolConstants.SwitchSessionStopCommandType)
+ {
+ JsonElement payload = request.GetProperty("payload");
+ result = switchControlSessions is null
+ ? CommandExecutionResult.Failure("unsupported_command", "PC Switch Control is unavailable.")
+ : SessionResult(await switchControlSessions.StopAsync(
+ auth.DeviceId ?? "",
+ payload.GetProperty("sessionId").GetString() ?? "",
+ payload.GetProperty("sequence").GetInt64(),
+ cancellationToken).ConfigureAwait(false));
+ }
+ else if (type == ProtocolConstants.GridSwitchSetCommandType && switchControlSessions is not null)
+ {
+ JsonElement payload = request.GetProperty("payload");
+ result = SessionResult(await switchControlSessions.ApplyLegacyGridEdgeAsync(
+ auth.DeviceId ?? "",
+ payload.TryGetProperty("sessionId", out JsonElement sessionId) ? sessionId.GetString() : null,
+ payload.TryGetProperty("sequence", out JsonElement sequence) ? sequence.GetInt64() : null,
+ payload.GetProperty("switchId").GetInt32(),
+ payload.GetProperty("state").GetString() == "down",
+ cancellationToken).ConfigureAwait(false));
+ }
+ else if (type == ProtocolConstants.GridSwitchSyncCommandType && switchControlSessions is not null)
+ {
+ JsonElement payload = request.GetProperty("payload");
+ result = SessionResult(await switchControlSessions.SynchronizeLegacyGridAsync(
+ auth.DeviceId ?? "",
+ payload.GetProperty("sessionId").GetString() ?? "",
+ payload.GetProperty("sequence").GetInt64(),
+ payload.GetProperty("pressedSwitchIds").EnumerateArray().Select(value => value.GetInt32()).ToHashSet(),
+ cancellationToken).ConfigureAwait(false));
+ }
+ else if (switchControlSessions?.IsActive == true &&
+ type != "connection.ping")
+ {
+ result = CommandExecutionResult.Failure(
+ "switch_session_active",
+ "Stop PC Switch Control before using other PC control commands.");
+ }
+ else if (type == "mouse.repeat.start")
{
if (mouseRepeatController is null)
{
@@ -169,6 +282,10 @@ public async Task EndControlSessionAsync(CancellationToken cancellationToken = d
{
await StopAllRepeatsAsync().ConfigureAwait(false);
await commandExecutor.ReleaseHeldInputsAsync(cancellationToken).ConfigureAwait(false);
+ if (switchControlSessions is not null)
+ {
+ await switchControlSessions.StopAllAsync(cancellationToken).ConfigureAwait(false);
+ }
commandExecutor.EndControlSession();
}
@@ -317,4 +434,40 @@ private static JsonObject PointerProfileResponse(string id, PointerMovementProfi
["error"] = null
};
}
+
+ private static CommandExecutionResult SessionResult(SwitchSessionResult result) =>
+ result.Ok
+ ? CommandExecutionResult.Success
+ : CommandExecutionResult.Failure(
+ result.Code ?? "command_failed",
+ result.Message ?? "PC Switch Control command failed.");
+
+ private static JsonObject SwitchProfileCatalogResponse(string id, SwitchControlProfileCatalog catalog)
+ {
+ return new JsonObject
+ {
+ ["version"] = ProtocolConstants.ProtocolVersion,
+ ["id"] = id,
+ ["type"] = ProtocolConstants.SwitchProfileListCommandType,
+ ["ok"] = true,
+ ["payload"] = new JsonObject
+ {
+ ["catalogRevision"] = catalog.CatalogRevision,
+ ["profiles"] = new JsonArray(catalog.Profiles.Select(profile => new JsonObject
+ {
+ ["id"] = profile.Id,
+ ["version"] = profile.Version,
+ ["name"] = profile.Name,
+ ["kind"] = profile.Kind,
+ ["bindings"] = new JsonArray(profile.Bindings.Select(binding => new JsonObject
+ {
+ ["switchId"] = binding.SwitchId,
+ ["label"] = binding.Label,
+ ["behavior"] = binding.Behavior
+ }).ToArray())
+ }).ToArray())
+ },
+ ["error"] = null
+ };
+ }
}
diff --git a/src/SwitchifyPc.Core/SwitchControl/SwitchControlSessionManager.cs b/src/SwitchifyPc.Core/SwitchControl/SwitchControlSessionManager.cs
index c85a694..2bd40d8 100644
--- a/src/SwitchifyPc.Core/SwitchControl/SwitchControlSessionManager.cs
+++ b/src/SwitchifyPc.Core/SwitchControl/SwitchControlSessionManager.cs
@@ -16,6 +16,8 @@ public sealed class SwitchControlSessionManager
private ActiveSession? active;
private long catalogRevision = 1;
+ public event Action? ActiveProfileChanged;
+
public SwitchControlSessionManager(
ISwitchControlProfileStore profiles,
ISwitchOutputSessionFactory outputs)
@@ -56,7 +58,16 @@ public async Task StartAsync(
await gate.WaitAsync(token).ConfigureAwait(false);
try
{
- await StopLockedAsync(token).ConfigureAwait(false);
+ try
+ {
+ await StopLockedAsync(token).ConfigureAwait(false);
+ }
+ catch
+ {
+ return SwitchSessionResult.Failure(
+ "output_failure",
+ "The previous switch output could not be released.");
+ }
ISwitchOutputSession output;
try
{
@@ -68,6 +79,7 @@ public async Task StartAsync(
}
active = new ActiveSession(deviceId, sessionId, profile, output);
+ ActiveProfileChanged?.Invoke(profile.Name);
return SwitchSessionResult.Success;
}
finally
@@ -116,7 +128,6 @@ public async Task StopAsync(
}
catch
{
- active = null;
return SwitchSessionResult.Failure("output_failure", "The active switch output could not be released.");
}
finally
@@ -240,11 +251,21 @@ private async Task EnsureLegacyGridSessionLockedAsync(
return SwitchSessionResult.Success;
}
- await StopLockedAsync(token).ConfigureAwait(false);
+ try
+ {
+ await StopLockedAsync(token).ConfigureAwait(false);
+ }
+ catch
+ {
+ return SwitchSessionResult.Failure(
+ "output_failure",
+ "The previous switch output could not be released.");
+ }
SwitchControlProfile profile = SwitchControlProfiles.BuiltIns[0];
try
{
active = new ActiveSession(deviceId, effectiveSessionId, profile, outputs.Create(profile));
+ ActiveProfileChanged?.Invoke(profile.Name);
return SwitchSessionResult.Success;
}
catch (DesktopInputException error)
@@ -282,10 +303,11 @@ private async Task ExecuteActiveLockedAsync(
private async Task StopLockedAsync(CancellationToken token)
{
ActiveSession? previous = active;
- active = null;
if (previous is not null)
{
await previous.Output.StopAsync(token).ConfigureAwait(false);
+ active = null;
+ ActiveProfileChanged?.Invoke(null);
}
}
diff --git a/src/SwitchifyPc.Core/Ui/MainWindowViewModel.cs b/src/SwitchifyPc.Core/Ui/MainWindowViewModel.cs
index d509318..12246c8 100644
--- a/src/SwitchifyPc.Core/Ui/MainWindowViewModel.cs
+++ b/src/SwitchifyPc.Core/Ui/MainWindowViewModel.cs
@@ -16,6 +16,7 @@ public sealed class MainWindowViewModel : INotifyPropertyChanged
};
private UpdateState updateState = UpdateState.CreateInitial("0.2.0");
private IReadOnlyList pairingApprovals = [];
+ private string? activeSwitchControlProfile;
public event PropertyChangedEventHandler? PropertyChanged;
@@ -57,6 +58,12 @@ public sealed class MainWindowViewModel : INotifyPropertyChanged
public string RecentBluetoothError => MainWindowCopy.BluetoothRecentError(bluetooth);
+ public bool HasActiveSwitchControlProfile => activeSwitchControlProfile is not null;
+
+ public string ActiveSwitchControlProfile => activeSwitchControlProfile is null
+ ? ""
+ : $"PC Switch Control: {activeSwitchControlProfile}";
+
public bool HasUpdateBanner => MainWindowCopy.UpdateBanner(updateState) is not null;
public string UpdateBannerTitle => MainWindowCopy.UpdateBanner(updateState)?.Title ?? "";
@@ -101,6 +108,13 @@ public void SetPairingApprovals(IReadOnlyList approv
OnPropertyChanged(nameof(ShowConnectedDeviceUi));
}
+ public void SetActiveSwitchControlProfile(string? profileName)
+ {
+ activeSwitchControlProfile = profileName;
+ OnPropertyChanged(nameof(HasActiveSwitchControlProfile));
+ OnPropertyChanged(nameof(ActiveSwitchControlProfile));
+ }
+
private void NotifyStatusChanged()
{
OnPropertyChanged(nameof(StatusTitle));
diff --git a/src/SwitchifyPc.Protocol/ProtocolConstants.cs b/src/SwitchifyPc.Protocol/ProtocolConstants.cs
index b1cd451..0bfe0c1 100644
--- a/src/SwitchifyPc.Protocol/ProtocolConstants.cs
+++ b/src/SwitchifyPc.Protocol/ProtocolConstants.cs
@@ -13,6 +13,11 @@ public static class ProtocolConstants
public const int MaxErrorMessageLength = 300;
public const string GridSwitchSetCommandType = "grid.switch.set";
public const string GridSwitchSyncCommandType = "grid.switch.sync";
+ public const string SwitchProfileListCommandType = "switch.profile.list";
+ public const string SwitchSessionStartCommandType = "switch.session.start";
+ public const string SwitchEdgeCommandType = "switch.edge";
+ public const string SwitchSyncCommandType = "switch.sync";
+ public const string SwitchSessionStopCommandType = "switch.session.stop";
public const int MinimumGridSwitchId = 1;
public const int MaximumGridSwitchId = 8;
@@ -41,6 +46,11 @@ public static class ProtocolConstants
"window.control",
GridSwitchSetCommandType,
GridSwitchSyncCommandType,
+ SwitchProfileListCommandType,
+ SwitchSessionStartCommandType,
+ SwitchEdgeCommandType,
+ SwitchSyncCommandType,
+ SwitchSessionStopCommandType,
"pointer.profile",
"pointer.display.move",
"pointer.speed.set",
@@ -72,7 +82,8 @@ public static class ProtocolConstants
"keyboard.textStream.key",
"media.control",
"window.control",
- GridSwitchSetCommandType
+ GridSwitchSetCommandType,
+ SwitchEdgeCommandType
};
public static readonly IReadOnlySet MouseButtons = new HashSet(StringComparer.Ordinal)
diff --git a/src/SwitchifyPc.Protocol/ProtocolValidator.cs b/src/SwitchifyPc.Protocol/ProtocolValidator.cs
index ecfc0f1..29e1fbc 100644
--- a/src/SwitchifyPc.Protocol/ProtocolValidator.cs
+++ b/src/SwitchifyPc.Protocol/ProtocolValidator.cs
@@ -185,10 +185,86 @@ private static ProtocolValidationResult ValidateCommandPayload(string type, Json
: Invalid("invalid_payload", "Window control action is invalid."),
ProtocolConstants.GridSwitchSetCommandType => ValidateGridSwitchSetPayload(payload),
ProtocolConstants.GridSwitchSyncCommandType => ValidateGridSwitchSyncPayload(payload),
+ ProtocolConstants.SwitchProfileListCommandType =>
+ ObjectPropertyCount(payload) == 0 ? Valid(payload) : Invalid("invalid_payload", "Payload must be empty."),
+ ProtocolConstants.SwitchSessionStartCommandType => ValidateSwitchSessionStartPayload(payload),
+ ProtocolConstants.SwitchEdgeCommandType => ValidateSwitchEdgePayload(payload),
+ ProtocolConstants.SwitchSyncCommandType => ValidateSwitchSyncPayload(payload),
+ ProtocolConstants.SwitchSessionStopCommandType => ValidateSwitchSessionStopPayload(payload),
_ => Invalid("invalid_type", "Unsupported message type.")
};
}
+ private static ProtocolValidationResult ValidateSwitchSessionStartPayload(JsonElement payload)
+ {
+ return ObjectPropertyCount(payload) == 4 &&
+ TryGetString(payload, "sessionId", out string? sessionId) &&
+ Guid.TryParse(sessionId, out _) &&
+ TryGetString(payload, "profileId", out string? profileId) &&
+ profileId.Length is >= 1 and <= 80 &&
+ TryGetInteger(payload, "profileVersion", out int profileVersion) &&
+ profileVersion > 0 &&
+ TryGetInteger(payload, "switchCount", out int switchCount) &&
+ switchCount is >= 1 and <= ProtocolConstants.MaximumGridSwitchId
+ ? Valid(payload)
+ : Invalid("invalid_payload", "Switch session start payload is invalid.");
+ }
+
+ private static ProtocolValidationResult ValidateSwitchEdgePayload(JsonElement payload)
+ {
+ return ObjectPropertyCount(payload) == 4 &&
+ ValidateSwitchSequence(payload) &&
+ TryGetInteger(payload, "switchId", out int switchId) &&
+ switchId is >= ProtocolConstants.MinimumGridSwitchId and <= ProtocolConstants.MaximumGridSwitchId &&
+ TryGetString(payload, "state", out string? state) &&
+ state is "down" or "up"
+ ? Valid(payload)
+ : Invalid("invalid_payload", "Switch edge payload is invalid.");
+ }
+
+ private static ProtocolValidationResult ValidateSwitchSyncPayload(JsonElement payload)
+ {
+ if (ObjectPropertyCount(payload) != 3 ||
+ !ValidateSwitchSequence(payload) ||
+ !payload.TryGetProperty("pressedSwitchIds", out JsonElement pressedSwitchIds) ||
+ pressedSwitchIds.ValueKind != JsonValueKind.Array ||
+ pressedSwitchIds.GetArrayLength() > ProtocolConstants.MaximumGridSwitchId)
+ {
+ return Invalid("invalid_payload", "Switch synchronization payload is invalid.");
+ }
+
+ int previous = 0;
+ foreach (JsonElement element in pressedSwitchIds.EnumerateArray())
+ {
+ if (element.ValueKind != JsonValueKind.Number ||
+ !element.TryGetInt32(out int switchId) ||
+ switchId is < ProtocolConstants.MinimumGridSwitchId or > ProtocolConstants.MaximumGridSwitchId ||
+ switchId <= previous)
+ {
+ return Invalid("invalid_payload", "Switch synchronization IDs must be unique and sorted.");
+ }
+ previous = switchId;
+ }
+ return Valid(payload);
+ }
+
+ private static ProtocolValidationResult ValidateSwitchSessionStopPayload(JsonElement payload)
+ {
+ return ObjectPropertyCount(payload) == 2 && ValidateSwitchSequence(payload)
+ ? Valid(payload)
+ : Invalid("invalid_payload", "Switch session stop payload is invalid.");
+ }
+
+ private static bool ValidateSwitchSequence(JsonElement payload)
+ {
+ return TryGetString(payload, "sessionId", out string? sessionId) &&
+ Guid.TryParse(sessionId, out _) &&
+ payload.TryGetProperty("sequence", out JsonElement sequence) &&
+ sequence.ValueKind == JsonValueKind.Number &&
+ sequence.TryGetInt64(out long value) &&
+ value > 0;
+ }
+
private static ProtocolValidationResult ValidateGridSwitchSetPayload(JsonElement payload)
{
bool basePayloadValid = TryGetInteger(payload, "switchId", out int switchId) &&
diff --git a/src/SwitchifyPc.Tests/ButtonStyleTests.cs b/src/SwitchifyPc.Tests/ButtonStyleTests.cs
index 0eb40fc..3e47f0a 100644
--- a/src/SwitchifyPc.Tests/ButtonStyleTests.cs
+++ b/src/SwitchifyPc.Tests/ButtonStyleTests.cs
@@ -2,6 +2,7 @@
using System.Windows;
using System.Windows.Media;
using SwitchifyPc.App;
+using SwitchifyPc.Core.SwitchControl;
using WpfBorder = System.Windows.Controls.Border;
using WpfControl = System.Windows.Controls.Control;
using WpfControlTemplate = System.Windows.Controls.ControlTemplate;
@@ -47,6 +48,24 @@ public void SettingsWindowPrimaryButtonStyleUsesRedHoverBrush()
});
}
+ [Fact]
+ public void ProfileWindowPrimaryButtonStyleUsesRedHoverBrush()
+ {
+ RunOnSta(() =>
+ {
+ WpfTestApplication.ApplyTheme(SwitchifyPc.App.Themes.AppTheme.Light);
+ SwitchControlProfileWindow window = new(new StaticProfileStore(), () => null);
+ try
+ {
+ AssertPrimaryButtonStyleUsesRedHoverBrush(window);
+ }
+ finally
+ {
+ window.Close();
+ }
+ });
+ }
+
private static void AssertPrimaryButtonStyleUsesRedHoverBrush(FrameworkElement element)
{
Style style = Assert.IsType