Files
fancywin/BUILD.md
T
2026-08-20 09:50:30 +01:00

14 KiB
Raw Blame History

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.

Requirements

  • Go 1.24 or later
  • PowerShell for the supplied Windows build script, or GNU Make and a POSIX shell for the Make targets
  • The Gitea tea CLI, authenticated for the repository, when publishing a release
  • 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

cmd/fancywin/                          CLI entry point
assets/fancywin.png                    Full-resolution generated icon source
internal/assets/                       Embedded runtime image assets
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:

.\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 remains a console-subsystem application so diagnostics work without a second binary. Normal execution calls FreeConsole and operates through the notification-area icon. --debug retains the console, while startup failures in detached mode are shown with MessageBoxW.

Cross-compiling

The standard x64 build can be produced from the repository root with:

make build

This runs the tests, cross-compiles dist/fancywin.exe, and copies the example configuration to dist/fancywin.yaml. Override GOARCH for another Windows architecture, for example make build GOARCH=arm64.

The equivalent commands are shown below for environments without GNU Make.

Build Windows x64 from Linux, macOS, or another Go-supported host:

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:

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:

go test -buildvcs=false ./...
go test -buildvcs=false -race ./...

Validate Windows-only code without executing it:

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:

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.
  12. Confirm the custom icon is crisp in the normal and overflow notification areas at 100%, 150%, and 200% scaling.
  13. Select each named layout from the tray, confirm the check mark and overlay change immediately, and verify active_layout changes without disturbing YAML comments or layout formatting.
  14. Right-click the tray icon, select Exit, and confirm the process and icon both disappear cleanly.

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. Creates a hidden window and registers a notification-area icon with Shell_NotifyIconW.
  4. Installs a SetWinEventHook covering EVENT_SYSTEM_MOVESIZESTART through EVENT_SYSTEM_MOVESIZEEND with WINEVENT_OUTOFCONTEXT and WINEVENT_SKIPOWNPROCESS.
  5. Registers configured system-wide hotkeys against the current thread.
  6. Creates a two-second timer for configuration reload checks and a 25 ms timer for sampling the mouse button, Shift, and active drag state.
  7. Enters a standard Win32 GetMessage loop.

The tray icon is a generated FancyWin mark embedded as a 64×64 RGBA PNG. At startup the standard-library PNG decoder supplies premultiplied BGRA pixels to a 32-bit GDI DIB section, which CreateIconIndirect converts to an alpha-aware native HICON; it never needs to be extracted to disk. A stock Windows icon is used only if conversion unexpectedly fails.

The tooltip contains the active layout. The context menu deduplicates configured layouts by case-insensitive name, checks the active one, and provides Exit. Selecting a layout updates runtime state immediately and rewrites only the top-level active_layout YAML line so the choice persists. Exit posts WM_QUIT to the existing message loop for orderly hook, hotkey, overlay, icon, and tray cleanup. A registered TaskbarCreated message restores the icon after Windows Explorer restarts.

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, active-layout existence, unique layout-name/monitor pairs, zone bounds, hotkey action syntax, and required values. Legacy files in which all layouts are unnamed and active_layout is absent are normalized to a single layout name of default before validation.

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

The release version is read from the version constant in cmd/fancywin/main.go. After committing and pushing that version, publish it with:

make release

The target performs a fresh x64 build, refuses to publish from a dirty or unpushed working tree, and creates a Gitea release and corresponding vVERSION tag through the authenticated tea CLI. The executable and its SHA-256 checksum are attached to the release; the YAML is not published, and users obtain the documented example configuration from the repository. REMOTE defaults to origin, and GOARCH defaults to amd64; either can be overridden on the command line.

For a distributable release, ensure that you have also:

  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 and test ARM64 separately when it is part of the release scope.
  4. Complete the Windows smoke-test checklist.
  5. 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.