This document describes the internal architecture of WindowAnchor v1.1.0 for contributors and maintainers.
┌─────────────────────────────────────────────────────────────────┐
│ UI Layer App.xaml.cs · SettingsWindow · Dialogs │
│ (WPF, tray) Owns no business logic. Calls Coordinator. │
├─────────────────────────────────────────────────────────────────┤
│ Coordinator LayoutCoordinator │
│ Wires display-change events → WorkspaceService│
│ Owns notification balloons. │
├─────────────────────────────────────────────────────────────────┤
│ Services WorkspaceService · MonitorService │
│ WindowService · StorageService │
│ JumpListService · TitleParser │
│ Pure logic, no UI dependencies. │
├─────────────────────────────────────────────────────────────────┤
│ Models WorkspaceSnapshot · WorkspaceEntry │
│ MonitorInfo · WindowRecord │
│ Plain data, no logic, no dependencies. │
├─────────────────────────────────────────────────────────────────┤
│ Native NativeMethods.Window · NativeMethods.Display │
│ All P/Invoke declarations. No logic. │
└─────────────────────────────────────────────────────────────────┘
Owns everything related to physical displays.
GetCurrentMonitorFingerprint()— callsQueryDisplayConfigto enumerate active display paths, extracts EDID manufacturer ID + product code + connector instance for each display, sorts them, joins them, and returns the first 8 hex characters of a SHA-256 hash. This hash is stable: it does not change when resolution or refresh rate changes, only when the set of physical monitors changes.GetCurrentMonitors()— returns aList<MonitorInfo>where each entry has a stableMonitorId(same EDID-based format as the fingerprint), a friendly name fromDisplayConfigGetDeviceInfo, geometry fromEnumDisplayMonitors, and a primary flag.GetMonitorForWindow(hWnd, monitors)— static helper, callsMonitorFromWindowto map a live HWND to a monitor in the supplied list.
Owns everything related to live windows.
SnapshotAllWindows(monitors?)— callsEnumWindows, filters viaShouldIncludeWindow, callsCaptureWindowRecordfor each visible window, and optionally tags each record with a monitor viaGetMonitorForWindow. Returns a flatList<WindowRecord>.ShouldIncludeWindow(hWnd)— excludes invisible, cloaked, zero-area, tool, and known-OS-chrome windows (class allow-list inOsWindowClassSkipList).CaptureWindowRecord(hWnd)— callsGetWindowPlacement(DPI-aware normalised rect),QueryFullProcessImageName,GetClassName,GetWindowText. Returns aWindowRecord.RestoreWindow(hWnd, record)— callsSetWindowPlacementthen, for maximised windows, a secondShowWindow(SW_MAXIMIZE)pass to ensure the maximised state is applied on the correct monitor.GetAllWindowsWithPids()— returns anHWND → (PID, WindowRecord)dictionary used byWorkspaceServiceduring the restore matching passes.
The main orchestration service. Called by LayoutCoordinator and directly by UI code.
TakeSnapshot(name, saveFiles, monitorIds, progress)— the save pipeline:- Get fingerprint and current monitors.
- Enumerate live windows.
- Filter to selected monitors (when
monitorIdsis not null). - For each window: Tier 1 title parse → Tier 2 jump-list lookup → Tier 3 file search.
- Build and return a
WorkspaceSnapshot(does not save to disk — caller decides).
RestoreWorkspaceAsync(snapshot, token)— the restore pipeline:- Launch any missing executables via
Process.Start(with savedLaunchArg). - Wait up to 8 seconds for them to create windows, polling
GetAllWindowsWithPids. - Match live HWNDs to
WorkspaceEntryrecords by executable path + class name. - Call
WindowService.RestoreWindowfor each matched pair. - Perform a second pass for windows that arrived late.
- Launch any missing executables via
RestoreWorkspaceSelectiveAsync(snapshot, monitorIds, token)— same as above but filters entries to the specified monitor IDs before restoring.
Plain JSON file I/O. No business logic.
- Storage paths (under
%AppData%\WindowAnchor\):workspaces/{name}.workspace.json— one file perWorkspaceSnapshot.last_fingerprint.txt— persists the last-known fingerprint across restarts..migrated_v2— sentinel written after the one-time v1→v2 migration.
MigrateToV2()— on first run, converts anyprofiles/*.profile.jsonlegacy files toWorkspaceSnapshotobjects. Runs once, guarded by the.migrated_v2sentinel.
Reacts to WM_DISPLAYCHANGE events forwarded from App.xaml.cs.
HandleDisplayChangeAsync()— debounces the event (1 s), computes the new fingerprint, looks up a matching workspace, and callsWorkspaceService.RestoreWorkspaceAsyncif one is found.- Owns all notification balloon calls via the private
NotifyBalloonhelper, which marshals to the UI thread.
Reads the Windows Jump-List AutoDestList binary files from %AppData%\Microsoft\Windows\Recent\AutomaticDestinations\ using the OpenMcdf library to extract recently-opened file paths per application.
Stateless utility class. ExtractFilePath(processName, titleSnippet) applies a set of regular expressions to the window title to extract a file path and returns a (path, confidence) tuple.
User clicks "Save Workspace"
→ SaveWorkspaceDialog collects name + monitor selection
→ WorkspaceService.TakeSnapshot(name, saveFiles, monitorIds, progress)
→ MonitorService.GetCurrentMonitors()
→ WindowService.SnapshotAllWindows(monitors)
→ JumpListService.BuildSnapshotCache()
→ per window: TitleParser + JumpListService → WorkspaceEntry
→ StorageService.SaveWorkspace(snapshot)
WM_DISPLAYCHANGE arrives at App.xaml.cs
→ LayoutCoordinator.HandleDisplayChangeAsync()
→ debounce 1 s
→ MonitorService.GetCurrentMonitorFingerprint()
→ WorkspaceService.FindWorkspaceByFingerprint(fingerprint)
→ WorkspaceService.RestoreWorkspaceAsync(snapshot)
→ launch missing apps
→ poll for new windows (up to 8 s)
→ WindowService.RestoreWindow() for each match
→ NotifyBalloon("Workspace Restored", ...)
| Type | Purpose |
|---|---|
WorkspaceSnapshot |
Top-level save artifact. Contains a list of MonitorInfo and a list of WorkspaceEntry. |
WorkspaceEntry |
One saved window: app identity, optional file path, window position, and monitor assignment. |
MonitorInfo |
Physical monitor metadata: stable ID, friendly name, geometry, index, primary flag. |
WindowRecord |
Captured state of a live window: DPI-aware normalised rect, class name, title snippet, process name, executable path. |
- Create
Services/MyService.csin the correct namespace (WindowAnchor.Services). - Add a
<summary>XML doc comment on the class and all public members. - Register the service as a singleton in
App.xaml.csalongside the existing services. - Inject it via the constructor of any service that needs it.
- Use
AppLogger.Info/AppLogger.Warnfor all diagnostic output — neverDebug.WriteLine.