feat: add portable Windows zone manager
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user