23dcfb393f
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>
93 lines
2.2 KiB
Go
93 lines
2.2 KiB
Go
package netcfg
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"gitea.apointless.space/bsncubed/ipswap/internal/preset"
|
|
)
|
|
|
|
// ConfirmText renders the before/after body of the confirmation MessageBox.
|
|
//
|
|
// The live side must be read at prompt time rather than taken from the cached
|
|
// active-preset poll: the prompt is the only safety net before a destructive
|
|
// change, and a 30-second-stale "CURRENT" block would undermine it.
|
|
func ConfirmText(p preset.Preset, live LiveConfig) string {
|
|
var b strings.Builder
|
|
|
|
fmt.Fprintf(&b, "Apply preset %q to adapter %q?\n\n", p.Name, p.Adapter)
|
|
|
|
b.WriteString("CURRENT\n")
|
|
writeLive(&b, live)
|
|
|
|
b.WriteString("\nNEW\n")
|
|
writePreset(&b, p)
|
|
|
|
return b.String()
|
|
}
|
|
|
|
func writeLive(b *strings.Builder, live LiveConfig) {
|
|
if len(live.Addresses) == 0 {
|
|
b.WriteString(" (no IPv4 address)\n")
|
|
}
|
|
for i, a := range live.Addresses {
|
|
suffix := ""
|
|
if live.DHCP {
|
|
suffix = " (DHCP)"
|
|
}
|
|
prefix := " "
|
|
if i > 0 {
|
|
prefix = " + "
|
|
}
|
|
fmt.Fprintf(b, "%s%s%s\n", prefix, a.String(), suffix)
|
|
}
|
|
if len(live.Gateways) > 0 {
|
|
fmt.Fprintf(b, " Gateway: %s\n", strings.Join(live.Gateways, ", "))
|
|
}
|
|
if len(live.DNS) > 0 {
|
|
suffix := ""
|
|
if live.DNSFromDHCP {
|
|
suffix = " (DHCP)"
|
|
}
|
|
fmt.Fprintf(b, " DNS: %s%s\n", strings.Join(live.DNS, ", "), suffix)
|
|
}
|
|
}
|
|
|
|
func writePreset(b *strings.Builder, p preset.Preset) {
|
|
if p.Mode == preset.ModeDHCP {
|
|
b.WriteString(" DHCP\n")
|
|
} else if p.Primary != nil {
|
|
fmt.Fprintf(b, " %s\n", p.Primary.String())
|
|
for _, s := range p.Secondary {
|
|
fmt.Fprintf(b, " + %s\n", s.String())
|
|
}
|
|
if p.Primary.Gateway != "" {
|
|
fmt.Fprintf(b, " Gateway: %s\n", p.Primary.Gateway)
|
|
}
|
|
}
|
|
|
|
switch p.DNS.Mode {
|
|
case preset.ModeDHCP:
|
|
b.WriteString(" DNS: from DHCP\n")
|
|
default:
|
|
fmt.Fprintf(b, " DNS: %s\n", strings.Join(p.DNS.Servers, ", "))
|
|
}
|
|
}
|
|
|
|
// Summary is the one-line description of a live config, used for the editor's
|
|
// adapter picker.
|
|
func (l LiveConfig) Summary() string {
|
|
if len(l.Addresses) == 0 {
|
|
return "no IPv4 address"
|
|
}
|
|
parts := make([]string, 0, len(l.Addresses))
|
|
for _, a := range l.Addresses {
|
|
parts = append(parts, a.String())
|
|
}
|
|
s := strings.Join(parts, ", ")
|
|
if l.DHCP {
|
|
s += " (DHCP)"
|
|
}
|
|
return s
|
|
}
|