Scaffold ipswap: tray-driven Windows IP preset switcher

Implements the design in claude.md as a building skeleton: pure Go, no cgo,
cross-compiles to a single Windows .exe from Linux.

Architecture follows the spec's deliberate split. The fast path is native —
tray icon, grouped submenus, a Win32 MessageBox showing a live before/after
diff, then netsh. The slow path is an embedded web editor served on a random
loopback port and opened in the default browser.

Two decisions worth recording:

Reads use GetAdaptersAddresses, writes use netsh. The spec left the
enumeration mechanism open; parsing `netsh show config` breaks on a
non-English Windows because the output is localised. DNS static-vs-DHCP
origin is not exposed by that API, so it comes from one registry read.

The netsh command plan is built in portable code. That puts the delete-every-
existing-address step — the one that stops secondary addresses leaking across
switches — under test without needing a Windows box.

The editor requires the session token in a header for mutations, not just the
cookie: any page in the browser can make it POST to 127.0.0.1 with the cookie
attached, but it cannot set a header. The updater refuses to install a release
that publishes no SHA256.

Not yet done: no group picker for export (the API supports it), no
single-instance guard, and internal/server/web/app.css is reconstructed from
the description in claude.md rather than the canonical apointless.css.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 13:29:35 +10:00
commit 23dcfb393f
46 changed files with 5782 additions and 0 deletions
+67
View File
@@ -0,0 +1,67 @@
VERSION ?= 0.1.0
# -H windowsgui suppresses the console window that would otherwise sit behind
# the tray icon. -s -w strips the symbol table and DWARF; nothing here needs a
# Go stack trace off a customer's laptop, and it roughly halves the binary.
LDFLAGS := -s -w -H windowsgui -X main.version=$(VERSION)
# Go is installed at ~/.local/go on this box and deliberately kept off PATH, so
# prefer that toolchain and fall back to whatever `go` is in PATH elsewhere.
GO ?= $(shell test -x $(HOME)/.local/go/bin/go && echo $(HOME)/.local/go/bin/go || echo go)
# Pinned and run through `go run` so CI needs no separate install step.
GOVERSIONINFO ?= $(GO) run github.com/josephspurrier/goversioninfo/cmd/goversioninfo@v1.4.1
SYSO := cmd/ipswap/resource_windows.syso
.PHONY: build syso test vet check dist sums clean help
help: ## List targets
@grep -hE '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | awk 'BEGIN{FS=":.*?## "}{printf " %-14s %s\n", $$1, $$2}'
# ipswap is pure Go with no cgo anywhere, which is the whole reason the tray is
# fyne.io/systray and the dialogs are raw user32 rather than a GUI toolkit:
# `GOOS=windows go build` cross-compiles straight from Linux, no Wine, no
# mingw, no fyne-cross.
build: syso ## Cross-compile bin/ipswap.exe from any host
@mkdir -p bin
GOOS=windows GOARCH=amd64 $(GO) build -trimpath -ldflags "$(LDFLAGS)" -o bin/ipswap.exe ./cmd/ipswap
# The .syso carries the icon, the version resource and — the part that matters
# — the manifest that asks for asInvoker and per-monitor DPI. A plain `go
# build` with no .syso produces a binary with the generic executable icon and
# the default (unaware, asInvoker-by-omission) manifest.
#
# -64 is not optional: goversioninfo emits a 386 object by default, and linking
# that into an amd64 build fails with "unknown relocation type 7".
syso: ## Generate the Windows resource object
cd cmd/ipswap && $(GOVERSIONINFO) \
-64 \
-o resource_windows.syso \
-platform-specific=false \
versioninfo.json
test: ## Run tests
$(GO) test ./...
vet: ## Run go vet for both targets
$(GO) vet ./...
GOOS=windows GOARCH=amd64 $(GO) vet ./...
# The Windows-only files (netsh, user32, the registry) are the bulk of the
# risk, and a Linux `go build` never looks at them. This is the cheap way to
# keep them compiling.
check: test vet ## Typecheck both targets and run tests
GOOS=windows GOARCH=amd64 $(GO) build ./...
dist: build sums ## Build the release artifacts into dist/
@echo "dist/ contains:" && ls -1 dist
sums: ## Write dist/SHA256SUMS for the built binary
@mkdir -p dist
@cp bin/ipswap.exe dist/ipswap.exe
cd dist && sha256sum ipswap.exe > SHA256SUMS
@cat dist/SHA256SUMS
clean: ## Remove build artifacts
rm -rf bin dist $(SYSO)