Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ A failed mutation or failed write does not replace committed settings and does n
| Persisted JSON DTOs and legacy-field migration | `PersistedSettingsMapper` |
| Schema, profile, assignment and reference normalization | explicit `ISettingsNormalizationPass` implementations |
| Runtime command/event façade | `RuntimeCoordinator` |
| Runtime target discovery and identity resolution | `IRuntimeTargetResolver` |
| Runtime user feedback and automatic-mode synchronization | `IRuntimeFeedback` |
| Overlay activation and automatic evaluation use cases | `RuntimeOverlayActivator` and `AutomaticActivationService` |
| Application assignment mutations and overlay scope | `ApplicationAssignmentService` |
| Visual-profile lifecycle and tuning | `VisualProfileManagementService` |
Expand Down Expand Up @@ -124,6 +126,15 @@ Each registered `VisualProfileDefinition` is the authority for its transform's e

`ApplicationIdentityCache` is an optimization, not a product source of truth. Entries are keyed by both PID and process creation time so a reused PID cannot inherit another process lifetime's identity.

## Runtime ports

Runtime orchestration uses two narrow ports instead of one mixed environment abstraction:

- `IRuntimeTargetResolver` owns target-window resolution, target support checks and application identity lookup;
- `IRuntimeFeedback` owns user notifications and synchronization of the tray's automatic-mode state.

`SightAdaptContext` is the composition root that creates the concrete delegate adapters. Runtime services can substitute either collaborator independently in tests, and `RuntimeCoordinator` no longer exposes the previous five-delegate constructor.

## Foreground and overlay lifecycle

The foreground tracker polls every 75 ms and publishes only a changed supported application handle. Native `#32768` popup-menu windows are deliberately rejected as application targets, so opening a menu does not replace the active assignment. When an enabled assignment exists, the context resolves the application profile, the inherited or explicit menu profile, and the overlay scope.
Expand Down Expand Up @@ -160,7 +171,9 @@ Raw imports exist only in `NativeInterop`. Production components depend on focus
- **transient** geometry, positioning, and source-update failures are diagnosed, hide the overlay, and allow a later timer tick to recover;
- **best effort** cleanup failures are diagnosed without replacing the primary application failure.

Every failure is emitted through the structured `Diagnostics` authority. Native menu detection, menu-overlay creation, and cross-filter refresh are subordinate operations. Their failure closes transient menu overlays and restores the primary overlay's self-filter instead of disabling the application correction.
Every native failure is emitted through the structured `Diagnostics` authority. `RuntimeOverlayActivator` also classifies top-level activation failures: expected native or target-availability failures are diagnosed, cleaned up best-effort, transitioned into a safe `Fault` state and surfaced through `IRuntimeFeedback`; contract or programming failures are diagnosed, cleaned up without replacing the primary exception, and rethrown. Automatic activation retains the failed target as the suppression target so a failing foreground window is not immediately retried in a loop.

Native menu detection, menu-overlay creation, and cross-filter refresh are subordinate operations. Their failure closes transient menu overlays and restores the primary overlay's self-filter instead of disabling the application correction.

`ShowWindow` and `InvalidateRect` are handled explicitly at their call sites because their Boolean return values do not represent a standard extended-error success contract.

Expand Down
22 changes: 11 additions & 11 deletions src/SightAdapt/AutomaticActivationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,19 @@ internal sealed class AutomaticActivationService
{
private readonly ApplicationStateController _stateController;
private readonly RuntimeOverlayActivator _overlayActivator;
private readonly IRuntimeEnvironment _environment;
private readonly IRuntimeTargetResolver _targetResolver;

public AutomaticActivationService(
ApplicationStateController stateController,
RuntimeOverlayActivator overlayActivator,
IRuntimeEnvironment environment)
IRuntimeTargetResolver targetResolver)
{
_stateController = stateController ??
throw new ArgumentNullException(nameof(stateController));
_overlayActivator = overlayActivator ??
throw new ArgumentNullException(nameof(overlayActivator));
_environment = environment ??
throw new ArgumentNullException(nameof(environment));
_targetResolver = targetResolver ??
throw new ArgumentNullException(nameof(targetResolver));
}

public void Evaluate(
Expand All @@ -29,7 +29,7 @@ public void Evaluate(
if (!settings.AutomaticMode ||
!_stateController.AllowsAutomaticActivation ||
currentState == ApplicationRunState.ManualActive ||
!_environment.IsSupportedTarget(targetWindow))
!_targetResolver.IsSupportedTarget(targetWindow))
{
return;
}
Expand All @@ -47,7 +47,7 @@ public void Evaluate(
}

var identity =
_environment.ResolveIdentity(targetWindow);
_targetResolver.ResolveIdentity(targetWindow);
if (identity is null)
{
DisableAutomaticOverlayIfActive(currentState);
Expand Down Expand Up @@ -79,7 +79,7 @@ public bool IsConfigured(
ArgumentNullException.ThrowIfNull(settings);

var identity =
_environment.ResolveIdentity(targetWindow);
_targetResolver.ResolveIdentity(targetWindow);
return identity is not null &&
ProfileResolver.FindEnabledAssignment(
settings,
Expand All @@ -101,7 +101,7 @@ ApplicationRunState.Emergency or
_stateController.ClearAutomaticSuppression();

var targetWindow =
_environment.ResolveTargetWindow();
_targetResolver.ResolveTargetWindow();
if (targetWindow != nint.Zero)
{
Evaluate(settings, targetWindow);
Expand Down Expand Up @@ -138,7 +138,7 @@ public void HandleSettingsChanged(
}

var targetWindow =
_environment.ResolveTargetWindow();
_targetResolver.ResolveTargetWindow();
if (targetWindow != nint.Zero)
{
Evaluate(settings, targetWindow);
Expand All @@ -151,14 +151,14 @@ private void RefreshManualOverlay(
var targetWindow =
_stateController.Current.TargetWindow;
if (targetWindow == nint.Zero ||
!_environment.IsSupportedTarget(targetWindow))
!_targetResolver.IsSupportedTarget(targetWindow))
{
_overlayActivator.Disable();
return;
}

var identity =
_environment.ResolveIdentity(targetWindow);
_targetResolver.ResolveIdentity(targetWindow);
var assignment = identity is null
? null
: ProfileResolver.FindAssignment(
Expand Down
2 changes: 1 addition & 1 deletion src/SightAdapt/OverlaySession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ private void ActivateNew(
primary.Show();
if (primary.IsDisposed)
{
throw new InvalidOperationException(
throw new RuntimeOverlayUnavailableException(
"The overlay target became unavailable during activation.");
}

Expand Down
65 changes: 23 additions & 42 deletions src/SightAdapt/RuntimeCoordinator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,67 +9,48 @@ internal sealed class RuntimeCoordinator
{
private readonly SettingsCoordinator _settingsCoordinator;
private readonly ApplicationStateController _stateController;
private readonly IRuntimeEnvironment _environment;
private readonly IRuntimeTargetResolver _targetResolver;
private readonly IRuntimeFeedback _feedback;
private readonly RuntimeOverlayActivator _overlayActivator;
private readonly AutomaticActivationService _automaticActivation;
private readonly Func<IReadOnlySightAdaptSettings> _readSettings;
private bool _committingSettings;

public RuntimeCoordinator(
SettingsCoordinator settingsCoordinator,
ApplicationStateController stateController,
IRuntimeOverlay overlay,
Func<nint> resolveTargetWindow,
Func<nint, bool> isSupportedTarget,
Func<nint, ApplicationIdentity?> resolveIdentity,
Action<string> showNotification,
Action<bool> synchronizeAutomaticMode)
: this(
settingsCoordinator,
stateController,
overlay,
new DelegateRuntimeEnvironment(
resolveTargetWindow,
isSupportedTarget,
resolveIdentity,
showNotification,
synchronizeAutomaticMode),
readSettings: null)
{
}

internal RuntimeCoordinator(
SettingsCoordinator settingsCoordinator,
ApplicationStateController stateController,
IRuntimeOverlay overlay,
IRuntimeEnvironment environment,
Func<IReadOnlySightAdaptSettings>? readSettings)
IRuntimeTargetResolver targetResolver,
IRuntimeFeedback feedback,
Func<IReadOnlySightAdaptSettings>? readSettings = null)
{
_settingsCoordinator = settingsCoordinator ??
throw new ArgumentNullException(nameof(settingsCoordinator));
_stateController = stateController ??
throw new ArgumentNullException(nameof(stateController));
_environment = environment ??
throw new ArgumentNullException(nameof(environment));
_targetResolver = targetResolver ??
throw new ArgumentNullException(nameof(targetResolver));
_feedback = feedback ??
throw new ArgumentNullException(nameof(feedback));
_readSettings = readSettings ??
(() => _settingsCoordinator.Current);
_overlayActivator = new RuntimeOverlayActivator(
stateController,
overlay,
environment);
feedback);
_automaticActivation = new AutomaticActivationService(
stateController,
_overlayActivator,
environment);
targetResolver);
}

public void ToggleForActiveWindow()
{
var targetWindow =
_environment.ResolveTargetWindow();
_targetResolver.ResolveTargetWindow();
if (targetWindow == nint.Zero)
{
_environment.ShowNotification(
_feedback.ShowNotification(
RuntimeMessages.NoSupportedWindow);
return;
}
Expand All @@ -93,7 +74,7 @@ public void ToggleForActiveWindow()
}

var identity =
_environment.ResolveIdentity(targetWindow);
_targetResolver.ResolveIdentity(targetWindow);
var assignment = identity is null
? null
: ProfileResolver.FindAssignment(
Expand All @@ -111,13 +92,13 @@ public void ToggleForActiveWindow()
public void ToggleActiveApplicationAssignment()
{
var targetWindow =
_environment.ResolveTargetWindow();
_targetResolver.ResolveTargetWindow();
var identity = targetWindow == nint.Zero
? null
: _environment.ResolveIdentity(targetWindow);
: _targetResolver.ResolveIdentity(targetWindow);
if (identity is null)
{
_environment.ShowNotification(
_feedback.ShowNotification(
RuntimeMessages.IdentityUnavailable);
return;
}
Expand Down Expand Up @@ -157,7 +138,7 @@ public void ToggleActiveApplicationAssignment()
_automaticActivation.HandleSettingsChanged(settings);
}

_environment.ShowNotification(
_feedback.ShowNotification(
RuntimeMessages.AssignmentToggled(result));
}

Expand All @@ -171,7 +152,7 @@ public void SetAutomaticMode(bool enabled)
var settings = ReadSettings();
if (!commit.Succeeded)
{
_environment.SynchronizeAutomaticMode(
_feedback.SynchronizeAutomaticMode(
settings.AutomaticMode);
ShowCommitError(commit.ErrorMessage);
return;
Expand Down Expand Up @@ -238,15 +219,15 @@ public void EmergencyDisable()

if (commit.Succeeded)
{
_environment.ShowNotification(
_feedback.ShowNotification(
"All overlays were disabled. Automatic mode is off.");
return;
}

var settings = ReadSettings();
_environment.SynchronizeAutomaticMode(
_feedback.SynchronizeAutomaticMode(
settings.AutomaticMode);
_environment.ShowNotification(
_feedback.ShowNotification(
"All overlays were disabled for this session, but " +
(commit.ErrorMessage ??
"automatic mode could not be saved."));
Expand Down Expand Up @@ -280,7 +261,7 @@ private SettingsCommitResult<T> CommitSettings<T>(

private void ShowCommitError(string? message)
{
_environment.ShowNotification(
_feedback.ShowNotification(
string.IsNullOrWhiteSpace(message)
? RuntimeMessages.SettingsChangeFailed
: message);
Expand Down
37 changes: 25 additions & 12 deletions src/SightAdapt/RuntimeEnvironment.cs
Original file line number Diff line number Diff line change
@@ -1,43 +1,39 @@
namespace SightAdapt;

internal interface IRuntimeEnvironment
internal interface IRuntimeTargetResolver
{
nint ResolveTargetWindow();

bool IsSupportedTarget(nint targetWindow);

ApplicationIdentity? ResolveIdentity(nint targetWindow);
}

internal interface IRuntimeFeedback
{
void ShowNotification(string message);

void SynchronizeAutomaticMode(bool enabled);
}

internal sealed class DelegateRuntimeEnvironment : IRuntimeEnvironment
internal sealed class DelegateRuntimeTargetResolver :
IRuntimeTargetResolver
{
private readonly Func<nint> _resolveTargetWindow;
private readonly Func<nint, bool> _isSupportedTarget;
private readonly Func<nint, ApplicationIdentity?> _resolveIdentity;
private readonly Action<string> _showNotification;
private readonly Action<bool> _synchronizeAutomaticMode;

public DelegateRuntimeEnvironment(
public DelegateRuntimeTargetResolver(
Func<nint> resolveTargetWindow,
Func<nint, bool> isSupportedTarget,
Func<nint, ApplicationIdentity?> resolveIdentity,
Action<string> showNotification,
Action<bool> synchronizeAutomaticMode)
Func<nint, ApplicationIdentity?> resolveIdentity)
{
_resolveTargetWindow = resolveTargetWindow ??
throw new ArgumentNullException(nameof(resolveTargetWindow));
_isSupportedTarget = isSupportedTarget ??
throw new ArgumentNullException(nameof(isSupportedTarget));
_resolveIdentity = resolveIdentity ??
throw new ArgumentNullException(nameof(resolveIdentity));
_showNotification = showNotification ??
throw new ArgumentNullException(nameof(showNotification));
_synchronizeAutomaticMode = synchronizeAutomaticMode ??
throw new ArgumentNullException(nameof(synchronizeAutomaticMode));
}

public nint ResolveTargetWindow() =>
Expand All @@ -48,6 +44,23 @@ public bool IsSupportedTarget(nint targetWindow) =>

public ApplicationIdentity? ResolveIdentity(nint targetWindow) =>
_resolveIdentity(targetWindow);
}

internal sealed class DelegateRuntimeFeedback :
IRuntimeFeedback
{
private readonly Action<string> _showNotification;
private readonly Action<bool> _synchronizeAutomaticMode;

public DelegateRuntimeFeedback(
Action<string> showNotification,
Action<bool> synchronizeAutomaticMode)
{
_showNotification = showNotification ??
throw new ArgumentNullException(nameof(showNotification));
_synchronizeAutomaticMode = synchronizeAutomaticMode ??
throw new ArgumentNullException(nameof(synchronizeAutomaticMode));
}

public void ShowNotification(string message) =>
_showNotification(message);
Expand Down
16 changes: 16 additions & 0 deletions src/SightAdapt/RuntimeOverlay.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
namespace SightAdapt;

internal sealed class RuntimeOverlayUnavailableException :
Exception
{
public RuntimeOverlayUnavailableException(string message)
: base(message)
{
}

public RuntimeOverlayUnavailableException(
string message,
Exception innerException)
: base(message, innerException)
{
}
}

internal sealed class OverlayActivationRequest
{
public OverlayActivationRequest(
Expand Down
Loading
Loading