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,101 @@
|
||||
package preset
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ImportMode is what to do with the presets already on disk.
|
||||
type ImportMode string
|
||||
|
||||
const (
|
||||
// ImportMerge keeps existing presets and adds the incoming ones.
|
||||
ImportMerge ImportMode = "merge"
|
||||
// ImportReplace discards everything already saved.
|
||||
ImportReplace ImportMode = "replace"
|
||||
)
|
||||
|
||||
// Export renders the presets as a shippable preset pack. Passing one or more
|
||||
// group names filters to just those groups; passing none exports everything.
|
||||
//
|
||||
// The output is the same shape as presets.json so a pack can be dropped
|
||||
// straight in as a starting file, and it stays hand-editable.
|
||||
func (s *Store) Export(groups ...string) ([]byte, error) {
|
||||
want := make(map[string]bool, len(groups))
|
||||
for _, g := range groups {
|
||||
want[strings.ToLower(strings.TrimSpace(g))] = true
|
||||
}
|
||||
|
||||
out := File{Version: SchemaVersion}
|
||||
for _, p := range s.All() {
|
||||
if len(want) > 0 && !want[strings.ToLower(p.GroupOrUngrouped())] {
|
||||
continue
|
||||
}
|
||||
out.Presets = append(out.Presets, p)
|
||||
}
|
||||
|
||||
b, err := json.MarshalIndent(out, "", " ")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("encoding export: %w", err)
|
||||
}
|
||||
return append(b, '\n'), nil
|
||||
}
|
||||
|
||||
// ImportResult reports what an import did, for the summary shown afterwards.
|
||||
type ImportResult struct {
|
||||
Added int `json:"added"`
|
||||
Replaced int `json:"replaced"`
|
||||
Renamed []string `json:"renamed,omitempty"`
|
||||
}
|
||||
|
||||
// Import validates an incoming preset file in full before touching anything,
|
||||
// then applies it. A file that fails validation changes nothing on disk.
|
||||
//
|
||||
// On merge, an incoming preset whose id already exists is kept alongside the
|
||||
// existing one: it gets a fresh id and "(imported)" appended to its name, so a
|
||||
// colleague's pack can never silently overwrite what you already had.
|
||||
func (s *Store) Import(data []byte, mode ImportMode) (ImportResult, error) {
|
||||
incoming, err := Decode(data)
|
||||
if err != nil {
|
||||
return ImportResult{}, err
|
||||
}
|
||||
|
||||
var res ImportResult
|
||||
|
||||
switch mode {
|
||||
case ImportReplace:
|
||||
s.mu.Lock()
|
||||
s.file = File{Version: SchemaVersion, Presets: incoming.Presets}
|
||||
res.Added = len(incoming.Presets)
|
||||
s.mu.Unlock()
|
||||
|
||||
case ImportMerge:
|
||||
s.mu.Lock()
|
||||
existing := make(map[string]bool, len(s.file.Presets))
|
||||
for _, p := range s.file.Presets {
|
||||
existing[p.ID] = true
|
||||
}
|
||||
for _, p := range incoming.Presets {
|
||||
if existing[p.ID] {
|
||||
p.ID = NewID()
|
||||
p.Name = p.Name + " (imported)"
|
||||
res.Renamed = append(res.Renamed, p.Name)
|
||||
}
|
||||
existing[p.ID] = true
|
||||
s.file.Presets = append(s.file.Presets, p)
|
||||
res.Added++
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
default:
|
||||
return ImportResult{}, fmt.Errorf("unknown import mode %q", mode)
|
||||
}
|
||||
|
||||
if err := s.Save(); err != nil {
|
||||
// Reload so the in-memory copy matches whatever is actually on disk.
|
||||
_ = s.Load()
|
||||
return ImportResult{}, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
Reference in New Issue
Block a user