306 lines
12 KiB
Markdown
306 lines
12 KiB
Markdown
# 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
|
||
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:
|
||
|
||
```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 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
|
||
|
||
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.
|
||
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
|
||
|
||
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.
|