package setup import ( "fmt" "strconv" "strings" "charm.land/huh/v2" "git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/config" ) // configureHosts edits a copy of the static hosts; the caller decides whether to keep it. func configureHosts(hosts []config.StaticHost) ([]config.StaticHost, error) { hosts = append([]config.StaticHost(nil), hosts...) for { action := "done" var options []huh.Option[string] for i, host := range hosts { index := strconv.Itoa(i) options = append(options, huh.NewOption("Edit "+host.Name+" ("+host.Address+")", "edit:"+index), huh.NewOption("SSH options for "+host.Name, "ssh:"+index), huh.NewOption("Remove "+host.Name, "remove:"+index)) } options = append(options, huh.NewOption("Add host", "add"), huh.NewOption("Back", "done")) err := huh.NewForm(huh.NewGroup(huh.NewSelect[string](). Title("Static hosts"). Description("Hosts that are not in NetBox. They are found before NetBox and work offline."). Options(options...). Value(&action))).Run() if err != nil { return nil, err } if action == "done" { return hosts, nil } if action == "add" { host, err := editHost(config.StaticHost{}, hosts, -1) if err != nil { return nil, err } hosts = append(hosts, host) continue } verb, index, _ := strings.Cut(action, ":") i, _ := strconv.Atoi(index) switch verb { case "edit": host, err := editHost(hosts[i], hosts, i) if err != nil { return nil, err } hosts[i] = host case "ssh": options, err := editSSHOptions("SSH options for "+hosts[i].Name, hosts[i].SSH) if err != nil { return nil, err } hosts[i].SSH = options case "remove": remove := false if err := huh.NewConfirm().Title("Remove " + hosts[i].Name + "?").Value(&remove).Run(); err != nil { return nil, err } if remove { hosts = append(hosts[:i], hosts[i+1:]...) } } } } func editHost(host config.StaticHost, hosts []config.StaticHost, index int) (config.StaticHost, error) { name, address, user := host.Name, host.Address, host.User port := "" if host.Port > 0 { port = strconv.Itoa(host.Port) } tags := strings.Join(host.Tags, ", ") candidate := func() config.StaticHost { out := host out.Name = strings.TrimSpace(name) out.Address = strings.TrimSpace(address) out.User = strings.TrimSpace(user) out.Port, _ = strconv.Atoi(strings.TrimSpace(port)) out.Tags = splitList(tags) return out } // huh validates the typed value before storing it, so each validator // checks the host with its own field taken from the argument. validate := func(set func(*config.StaticHost, string)) func(string) error { return func(value string) error { h := candidate() set(&h, strings.TrimSpace(value)) return validateHostIn(h, hosts, index) } } err := huh.NewForm(huh.NewGroup( huh.NewInput().Title("Name").Description("What you type: netssh NAME. Letters, digits, dots, hyphens.").Value(&name). Validate(validate(func(h *config.StaticHost, v string) { h.Name = v })), huh.NewInput().Title("Address").Description("IP address or DNS name to connect to.").Value(&address). Validate(validate(func(h *config.StaticHost, v string) { h.Address = v if v == "" { h.Address = "-" // required: reported as invalid address } })), huh.NewInput().Title("User").Description("Leave empty for ssh.default_user.").Value(&user). Validate(validate(func(h *config.StaticHost, v string) { h.User = v })), huh.NewInput().Title("Port").Description("Leave empty for ssh.default_port or 22.").Value(&port).Validate(func(s string) error { if s = strings.TrimSpace(s); s == "" { return nil } if n, err := strconv.Atoi(s); err != nil || n < 1 || n > 65535 { return fmt.Errorf("must be a port number between 1 and 65535") } return nil }), huh.NewInput().Title("Tags").Description("Comma-separated, for ssh.rules match: {tag: …}.").Value(&tags), ).Title("Static host")).Run() if err != nil { return host, err } return candidate(), nil } // validateHostIn checks host as entry index of hosts (-1 for a new host). func validateHostIn(host config.StaticHost, hosts []config.StaticHost, index int) error { others := make([]config.StaticHost, 0, len(hosts)+1) for i, other := range hosts { if i != index { others = append(others, other) } } if host.Address == "" { host.Address = "placeholder" // reported once the address field is filled } return (&config.Config{Hosts: append(others, host)}).ValidateHosts() }