feat: add portable Windows zone manager

This commit is contained in:
Steve Cliff
2026-08-20 08:57:41 +01:00
parent 318300ef79
commit 6ddecad74d
15 changed files with 1760 additions and 4 deletions
+276
View File
@@ -0,0 +1,276 @@
# Building and developing FancyWin
This document covers source builds, cross-compilation, verification, release
artifacts, dependencies, and the Windows implementation. End-user instructions
are in [README.md](README.md).
## Requirements
- Go 1.24 or later
- PowerShell for the supplied Windows build script, or a POSIX shell for manual
cross-compilation
- Windows 11 for runtime integration testing
The project uses pure Go and sets `CGO_ENABLED=0`. No C compiler, Windows SDK,
.NET SDK, Visual Studio, or PowerToys checkout is required.
## Repository layout
```text
cmd/fancywin/ CLI entry point
internal/config/ YAML model, loading, and validation
internal/layout/ Platform-neutral zone geometry
internal/platform/platform_windows.go Win32 backend
internal/platform/platform_other.go Non-Windows diagnostic stub
fancywin.example.yaml Example user configuration
build.ps1 Native Windows build script
dist/ Generated portable artifacts
```
## Dependencies
Runtime functionality uses the Windows system DLLs available with Windows 11.
There are two pinned Go module dependencies:
- `golang.org/x/sys/windows` for Windows handles, callbacks, process queries,
and UTF-16 helpers
- `gopkg.in/yaml.v3` for strict YAML parsing
Both modules are statically linked into the executable. Exact versions and
checksums are recorded in `go.mod` and `go.sum`.
## Native Windows build
From PowerShell in the repository root:
```powershell
.\build.ps1
```
The script:
1. Selects `amd64` or `arm64` from `PROCESSOR_ARCHITECTURE`.
2. Sets `GOOS=windows` and disables CGO.
3. Runs the complete Go test suite.
4. Builds a stripped, reproducible-path executable.
5. Writes `dist\fancywin.exe` and copies the example to
`dist\fancywin.yaml`.
The generated executable is a console subsystem application so users can see
configuration and Win32 errors directly.
## Cross-compiling
Build Windows x64 from Linux, macOS, or another Go-supported host:
```sh
mkdir -p dist
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build \
-buildvcs=false \
-trimpath \
-ldflags='-s -w' \
-o dist/fancywin.exe \
./cmd/fancywin
cp fancywin.example.yaml dist/fancywin.yaml
```
For Windows on ARM:
```sh
CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build \
-buildvcs=false \
-trimpath \
-ldflags='-s -w' \
-o dist/fancywin-arm64.exe \
./cmd/fancywin
```
`-trimpath` removes local source paths. `-s -w` removes symbol and DWARF tables
to reduce the portable binary size. `-buildvcs=false` permits builds from source
archives or workspaces without complete Git metadata.
## Verification
Run platform-neutral tests and race detection on the development host:
```sh
go test -buildvcs=false ./...
go test -buildvcs=false -race ./...
```
Validate Windows-only code without executing it:
```sh
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go vet -buildvcs=false ./...
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -buildvcs=false ./cmd/fancywin
GOOS=windows GOARCH=arm64 CGO_ENABLED=0 go build -buildvcs=false ./cmd/fancywin
```
Validate the example configuration on any supported development host:
```sh
go run -buildvcs=false ./cmd/fancywin \
-config fancywin.example.yaml \
-check
```
Cross-compilation proves that the Windows API bindings type-check; it does not
replace runtime testing on a Windows desktop.
## Windows smoke-test checklist
Test both x64 and ARM64 where hardware is available:
1. Start FancyWin without elevation and confirm the monitor names and work areas
are printed.
2. Confirm a second instance exits with a single-instance error.
3. Shift-drag Win32, UWP/Windows App SDK, and Chromium-based windows into each
zone.
4. Exercise all configured hotkey actions, including wrap-around.
5. Test monitors with negative virtual-screen coordinates, different DPI
scaling, portrait orientation, and taskbars on different edges.
6. Maximize a window and then move it by hotkey, checking visible-frame
alignment.
7. Edit valid YAML and confirm it reloads within roughly two seconds.
8. Introduce invalid YAML and confirm the previous configuration stays active.
9. Verify exact and substring exclusions.
10. Confirm a normal process cannot move an elevated application and that a
same-integrity elevated run can do so.
11. Disconnect and reconnect a monitor and confirm current work-area information
is used for subsequent moves.
## Architecture
### Process model
FancyWin is a single process and a single portable executable. The main
goroutine is locked to its OS thread because Windows delivers the out-of-context
accessibility callback and thread hotkey messages through that thread's message
queue. The program does not inject code into other processes or install a hook
DLL, service, shell extension, or driver.
At startup the backend:
1. Requests per-monitor-v2 DPI awareness.
2. Creates a named mutex for single-instance enforcement.
3. Installs a `SetWinEventHook` covering `EVENT_SYSTEM_MOVESIZESTART` through
`EVENT_SYSTEM_MOVESIZEEND` with `WINEVENT_OUTOFCONTEXT` and
`WINEVENT_SKIPOWNPROCESS`.
4. Registers configured system-wide hotkeys against the current thread.
5. Creates a two-second timer for configuration reload checks and a 25 ms timer
for sampling the mouse button, Shift, and active drag state.
6. Enters a standard Win32 `GetMessage` loop.
### Mouse snapping
The move-start callback records a manageable top-level window. On move-end, the
backend uses the most recently sampled asynchronous Shift-key state, obtains the
pointer's monitor, resolves the applicable layout, and finds the first zone
containing the pointer. Sampling throughout the drag avoids losing activation
when Windows delivers the move-end accessibility event just after a key-state
transition. The backend then converts that percentage zone into the monitor work
area's physical coordinates and calls `SetWindowPos`.
The configured gap affects the destination rectangle but not zone hit testing.
This avoids dead pointer strips between adjacent zones.
Some Windows environments do not deliver the move/size accessibility events
reliably. The 25 ms input timer provides an independent fallback: at the initial
left-button press it records the manageable top-level window beneath the
pointer, observes whether that window's rectangle actually changes while the
button remains down, tracks Shift, and snaps on release. Ordinary clicks are
discarded because the recorded rectangle did not move. Coordination state
prevents the accessibility and polling paths from snapping the same drag twice.
### Keyboard snapping
`RegisterHotKey` posts `WM_HOTKEY` to the message-loop thread. The backend obtains
the foreground window and its nearest monitor. Direct `zone_N` actions use a
one-based index. Previous/next actions find the zone whose center is closest to
the current window center and wrap through the monitor's ordered zone list.
`MOD_NOREPEAT` prevents a held shortcut from generating repeated moves.
### Coordinates and visible frames
Zone percentages are resolved against `MONITORINFOEX.rcWork`, not the complete
monitor rectangle, so taskbar and app-bar space is excluded. Per-monitor-v2 DPI
awareness keeps pointer, monitor, and window coordinates in one physical-pixel
coordinate system across mixed-DPI displays.
Windows 11 commonly includes an invisible resize border in `GetWindowRect`.
Before moving a window, FancyWin compares that rectangle with
`DWMWA_EXTENDED_FRAME_BOUNDS` and expands the outer `SetWindowPos` rectangle so
the visible frame aligns with the requested zone.
### Overlay rendering
The overlay is implemented directly with User32 and GDI. While an active mouse
drag and Shift are both detected, FancyWin creates one borderless popup window
covering the usable work area of the monitor beneath the pointer. The window
uses `WS_EX_LAYERED`, `WS_EX_TRANSPARENT`, `WS_EX_TOOLWINDOW`,
`WS_EX_NOACTIVATE`, and `WS_EX_TOPMOST` so it remains visible without receiving
input, taking focus, or appearing in Alt+Tab.
The background is painted with a color key made transparent by
`SetLayeredWindowAttributes`. GDI then draws the gap-adjusted zone rectangles
and centered Segoe UI labels in the configured color and opacity. The overlay is
destroyed when Shift or the left mouse button is released. Moving the pointer to
another monitor recreates it with that monitor's work area and selected layout.
### Window filtering and permissions
The backend ignores invisible windows, child windows, owned windows, and
configured executable exclusions. Process names are obtained with
`PROCESS_QUERY_LIMITED_INFORMATION` and `QueryFullProcessImageName`.
Windows User Interface Privilege Isolation prevents a lower-integrity process
from reliably controlling a higher-integrity window. FancyWin does not attempt
to bypass that boundary or request elevation through a manifest.
### Configuration reload
The YAML decoder rejects unknown fields. Semantic validation checks the schema
version, gap range, unique monitor entries, zone bounds, hotkey action syntax,
and required values.
The message-loop timer checks the configuration modification time. A valid new
configuration replaces the active one and re-registers hotkeys. If parsing,
validation, or hotkey registration fails, the previous valid configuration and
hotkeys are restored.
## Current scope
The current backend intentionally excludes:
- A visual layout editor
- Low-level keyboard interception of Windows-reserved snap shortcuts
- Multi-zone selection and expansion
- Moving windows across monitors as part of next/previous traversal
- Persistence of application-to-zone history
- Automatic placement of newly created windows
- Virtual-desktop-specific layouts
- Layout switching shortcuts
- Rounded-corner control
These should be treated as separate features because several require additional
state, UI surfaces, or lower-level input handling.
## Release preparation
For a distributable release:
1. Run all tests, vet, both architecture builds, and the Windows smoke-test
checklist.
2. Update the `version` constant in `cmd/fancywin/main.go`.
3. Build x64 and ARM64 artifacts with `CGO_ENABLED=0`, `-trimpath`, and
`-ldflags='-s -w'`.
4. Include an example renamed to `fancywin.yaml` beside each executable.
5. Record SHA-256 hashes with `Get-FileHash` or `sha256sum`.
6. Code-sign public binaries if a trusted signing certificate and release
process are available. Signing is not required for portability, but unsigned
downloads may receive stronger Microsoft Defender SmartScreen warnings.
PowerToys was used as behavioral and architectural reference material only.
FancyWin is an independent implementation and contains no copied PowerToys
source code.