Skip to content

Domain Model and ADTs

Yasunobu edited this page Jun 26, 2026 · 1 revision

Domain Model & 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.

The types

InputTarget — what to press

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.

PressMode — how to press

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.

EngineState — what the engine is doing

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.

PressSpec — the whole instruction

public sealed record PressSpec(InputTarget Target, PressMode Mode);

A target paired with a mode — the complete description of one pressing session.

Primitives

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.

Why this matters

  • Exhaustivenessswitch over PressMode / InputTarget covers every case; the default arm throws UnreachableException because 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-side SpecBuilder does the user-facing validation before building a PressSpec.
  • 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. See Selection.cs and SpecBuilder.

Related: Architecture · Threading & ConfigureAwait

Clone this wiki locally