Skip to content

Architecture

Yasunobu edited this page Jun 26, 2026 · 1 revision

Architecture

KeepPressing is split into a pure domain core with zero Windows dependencies and a WinUI 3 app that wraps the OS. The core talks to the outside world through a single port — a lightweight hexagonal ("ports & adapters") arrangement.

  KeepPressing (WinUI 3 app)                         KeepPressing.Core
  ┌───────────────────────────────────┐              ┌─────────────────────────────┐
  │  MainPage  ──  MainPageViewModel   │              │  PressEngine (state machine)│
  │              (CommunityToolkit.Mvvm)│             │  PressSpec                  │
  │                  │                  │   uses      │  InputTarget (Mouse | Key)  │
  │     ┌────────────┼────────────┐     │  ─────────▶ │  PressMode   (Repeat | Hold)│
  │     ▼            ▼            ▼     │              │  EngineState (Idle | Running)│
  │ Presentation  Interop      DI      │              │                             │
  │ (SpecBuilder) (Win32)  (Composition)│             │  IInputSynthesizer  ◀── port│
  └───────────────────┬────────────────┘             └───────────▲─────────────────┘
                      │ implements                                 │
                      └───────────  Win32InputSynthesizer  ────────┘
                                    (SendInput adapter)

Layers

KeepPressing.Core — pure domain

No reference to Win32 or WinUI. It owns the state machine (PressEngine), the domain types (InputTarget, PressMode, EngineState, PressSpec, and primitives like ScreenPoint/KeyCode), and one port: IInputSynthesizer. All side effects flow through that port, which makes the engine fully unit-testable with a fake. Timing is injected via TimeProvider (real PeriodicTimer in production, FakeTimeProvider in tests). See Domain Model & ADTs.

KeepPressing/Interop — Win32 confined here

Every P/Invoke lives behind an interface in this folder: Win32InputSynthesizer (the SendInput adapter for the port), HotkeyListener (global hotkeys), CursorLocator (GetCursorPos), ImeGuard, WindowHandleProvider, plus the VirtualScreen multi-monitor coordinate helper. Native bindings are generated by CsWin32 from Interop/NativeMethods.txt. See Win32 Interop.

KeepPressing/Presentation — UI ↔ domain translation

Pure functions that map UI state to/from domain types: SpecBuilder turns the UI selection (PressConfiguration) into a validated PressSpec, and SpecDescriber turns engine state into the human-readable status text. Both take an ILocalizer, so they're testable without the WinUI resource system.

KeepPressing/ViewModels — MVVM

MainPageViewModel (built on CommunityToolkit.Mvvm [ObservableProperty] / [RelayCommand]) holds UI state, owns the commands, and marshals every event from Interop/Core (Pressed, StateChanged, Faulted) back onto the UI thread via IUiDispatcher. Selection.cs defines the UI-only choice types (TargetKind, PressModeKind, Choice<T>, HotkeyChoice).

Composition root & lifetime

App.OnLaunched builds the DI container via ServiceCollectionExtensions.AddKeepPressing. The input synthesizer, hotkey listener, cursor locator, IME guard, dispatcher, localizer and the PressEngine are all singletons (each owns a single piece of process state — a dedicated thread or the running loop); the MainPageViewModel is transient. Because WinUI Pages are constructed parameterlessly, MainPage resolves its ViewModel from App.Services.

On window close the app disposes the container asynchronously so that PressEngine.DisposeAsync can wait for any held key/button to be released first. See Threading & ConfigureAwait.

Design principles at a glance

  • Make illegal states unrepresentable — closed ADTs for targets/modes/state.
  • One side-effect boundary — everything OS-touching is behind IInputSynthesizer (or an Interop interface).
  • Single source of truth for choices — option lists live in the ViewModel, bound by XAML, not duplicated in markup.

Deeper dives: Domain Model & ADTs · Threading & ConfigureAwait · Win32 Interop

Clone this wiki locally