-
Notifications
You must be signed in to change notification settings - Fork 0
Domain Model and ADTs
KeepPressing.Core models the problem with closed algebraic data types (ADTs) — abstract record
hierarchies with a private constructor so the only subtypes are the ones declared inside. This makes
switch expressions exhaustive and, more importantly, makes illegal states unrepresentable.
public abstract record InputTarget
{
private InputTarget() { }
public sealed record Mouse(MouseButton Button, ScreenPoint? Position = null) : InputTarget;
public sealed record Key(KeyCode Code) : InputTarget;
}A target is either a mouse button (with an optional fixed Position; null means "current cursor
position") or a key. There is no third case, and no way to construct an invalid one.
public abstract record PressMode
{
private PressMode() { }
public sealed record Repeat : PressMode // carries a positive Interval (validated in ctor)
public sealed record Hold : PressMode // singleton: PressMode.Hold.Instance
}Repeat carries an interval and validates it is positive in its constructor; Hold carries no
data and is a singleton. You cannot express "hold mode, but with an interval" — that nonsensical state
simply doesn't exist in the type system.
public abstract record EngineState
{
private EngineState() { }
public sealed record Idle : EngineState // singleton: EngineState.Idle.Instance
public sealed record Running(PressSpec Spec) : EngineState // snapshot of what's running
}Two states only. Running carries a snapshot of the spec it's executing.
public sealed record PressSpec(InputTarget Target, PressMode Mode);A target paired with a mode — the complete description of one pressing session.
public readonly record struct ScreenPoint(int X, int Y); // virtual-screen physical pixels
public readonly record struct KeyCode(ushort Value); // opaque Win32 VK; Core never interprets it
public readonly record struct VirtualScreenRect(int Left, int Top, int Width, int Height);
public enum MouseButton { Left, Right, Middle }Notably, KeyCode is opaque to Core: it's just a ushort that Core carries to the synthesizer
without interpretation, which keeps all Win32 virtual-key knowledge out of the domain.
-
Exhaustiveness —
switchoverPressMode/InputTargetcovers every case; thedefaultarm throwsUnreachableExceptionbecause the closed hierarchy guarantees it can't be hit. -
Validation at the boundary — invalid input is rejected when constructing domain types (e.g.
Repeat's positive-interval check), so the engine only ever runs well-formed specs. The UI-sideSpecBuilderdoes the user-facing validation before building aPressSpec. -
UI state is kept separate — the UI needs "in-progress" selections that aren't yet a valid domain
value (e.g. the Keyboard tab is selected but no key is set), so the ViewModel uses its own enums
(
TargetKind,PressModeKind) and only translates to the ADT when starting. SeeSelection.csandSpecBuilder.
Related: Architecture · Threading & ConfigureAwait
KeepPressing · Releases · Issues · Licensed under Apache-2.0
Getting started
User guide
Developer guide