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:
@@ -0,0 +1,80 @@
|
||||
//go:build windows
|
||||
|
||||
// Package dialog wraps the handful of Win32 message boxes ipswap needs.
|
||||
//
|
||||
// This is raw user32 rather than a GUI toolkit on purpose: the fast path must
|
||||
// stay native and dependency-free, and a confirmation prompt is the only
|
||||
// blocking UI the tray side has.
|
||||
package dialog
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var (
|
||||
user32 = syscall.NewLazyDLL("user32.dll")
|
||||
messageBoxW = user32.NewProc("MessageBoxW")
|
||||
)
|
||||
|
||||
// MessageBoxW uType flags.
|
||||
const (
|
||||
mbOK = 0x00000000
|
||||
mbOKCancel = 0x00000001
|
||||
mbYesNo = 0x00000004
|
||||
mbIconError = 0x00000010
|
||||
mbIconQuestion = 0x00000020
|
||||
mbIconWarning = 0x00000030
|
||||
mbIconInformation = 0x00000040
|
||||
// The tray has no window to own the dialog, so without TOPMOST the box can
|
||||
// open behind whatever is focused and look like a hang.
|
||||
mbSetForeground = 0x00010000
|
||||
mbTopMost = 0x00040000
|
||||
)
|
||||
|
||||
// MessageBoxW return values.
|
||||
const (
|
||||
idOK = 1
|
||||
idYes = 6
|
||||
)
|
||||
|
||||
func show(title, text string, flags uint32) int {
|
||||
t, err := syscall.UTF16PtrFromString(text)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
c, err := syscall.UTF16PtrFromString(title)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
ret, _, _ := messageBoxW.Call(
|
||||
0,
|
||||
uintptr(unsafe.Pointer(t)),
|
||||
uintptr(unsafe.Pointer(c)),
|
||||
uintptr(flags|mbSetForeground|mbTopMost),
|
||||
)
|
||||
return int(ret)
|
||||
}
|
||||
|
||||
// Confirm asks a yes/no question and reports whether the user said yes.
|
||||
func Confirm(title, text string) bool {
|
||||
return show(title, text, mbYesNo|mbIconQuestion) == idYes
|
||||
}
|
||||
|
||||
// Info shows a plain acknowledgement box.
|
||||
func Info(title, text string) {
|
||||
show(title, text, mbOK|mbIconInformation)
|
||||
}
|
||||
|
||||
// Error shows a failure. The caller is expected to include the raw netsh
|
||||
// output — that text is the only diagnostic the user has on site.
|
||||
func Error(title, text string) {
|
||||
show(title, text, mbOK|mbIconError)
|
||||
}
|
||||
|
||||
// ErrorWithRetryAsAdmin reports a failure and offers to relaunch elevated.
|
||||
// It reports whether the user accepted.
|
||||
func ErrorWithRetryAsAdmin(title, text string) bool {
|
||||
return show(title, text+"\n\nRelaunch ipswap as administrator and try again?",
|
||||
mbOKCancel|mbIconWarning) == idOK
|
||||
}
|
||||
Reference in New Issue
Block a user