Gitfed
bastien-mrq/sssh / internal / tui / form.go
package tui

import (
	"errors"
	"fmt"
	"strconv"
	"strings"

	"github.com/charmbracelet/huh"

	"git.neuromancer.ovh/bastien-mrq/sssh/internal/host"
)

// ErrAborted est rendu quand l'utilisateur annule un formulaire.
var ErrAborted = errors.New("annulé")

// HostForm ouvre le formulaire d'ajout/édition et modifie h en place.
// oldName est vide pour un ajout ; exists vérifie l'unicité du nom.
func HostForm(h *host.Host, oldName string, exists func(string) bool) error {
	name := h.Name
	port := ""
	if h.Port != 0 {
		port = strconv.Itoa(h.Port)
	}
	tags := strings.Join(h.Tags, ",")

	title := "Nouveau host"
	if oldName != "" {
		title = "Édition de " + oldName
	}

	form := huh.NewForm(huh.NewGroup(
		huh.NewInput().Title("Nom").Description(title).Value(&name).
			Validate(func(s string) error {
				s = strings.TrimSpace(s)
				if s == "" {
					return errors.New("le nom est obligatoire")
				}
				if strings.ContainsAny(s, " \t") {
					return errors.New("pas d'espaces dans le nom")
				}
				if s != oldName && exists(s) {
					return fmt.Errorf("%q existe déjà", s)
				}
				return nil
			}),
		huh.NewInput().Title("Hôte (IP ou domaine)").Value(&h.Host).
			Validate(func(s string) error {
				if strings.TrimSpace(s) == "" {
					return errors.New("l'hôte est obligatoire")
				}
				return nil
			}),
		huh.NewInput().Title("Utilisateur").Value(&h.User),
		huh.NewInput().Title("Port (défaut 22)").Value(&port).
			Validate(func(s string) error {
				if strings.TrimSpace(s) == "" {
					return nil
				}
				if _, err := strconv.Atoi(strings.TrimSpace(s)); err != nil {
					return errors.New("port numérique attendu")
				}
				return nil
			}),
		huh.NewInput().Title("Clé privée (optionnel)").Placeholder("~/.ssh/id_ed25519").Value(&h.Identity),
		huh.NewInput().Title("Jump host / ProxyJump (optionnel)").Value(&h.Jump),
		huh.NewInput().Title("Tags, séparés par des virgules (optionnel)").Value(&tags),
	))

	if err := form.Run(); err != nil {
		if errors.Is(err, huh.ErrUserAborted) {
			return ErrAborted
		}
		return err
	}

	h.Name = strings.TrimSpace(name)
	h.Host = strings.TrimSpace(h.Host)
	h.User = strings.TrimSpace(h.User)
	h.Identity = strings.TrimSpace(h.Identity)
	h.Jump = strings.TrimSpace(h.Jump)
	h.Port = 0
	if p := strings.TrimSpace(port); p != "" {
		h.Port, _ = strconv.Atoi(p)
	}
	h.Tags = nil
	for _, t := range strings.Split(tags, ",") {
		if t = strings.TrimSpace(t); t != "" {
			h.Tags = append(h.Tags, t)
		}
	}
	return nil
}

// Confirm pose une question oui/non ; rend false si l'utilisateur annule.
func Confirm(question string) bool {
	ok := false
	err := huh.NewConfirm().Title(question).Affirmative("Oui").Negative("Non").Value(&ok).Run()
	return err == nil && ok
}

// SelectSSHConfigHosts propose une multi-sélection parmi les hosts
// de ~/.ssh/config non encore enregistrés, et rend ceux choisis.
func SelectSSHConfigHosts(candidates []host.Host) ([]host.Host, error) {
	byName := map[string]host.Host{}
	opts := make([]huh.Option[string], len(candidates))
	for i, h := range candidates {
		byName[h.Name] = h
		opts[i] = huh.NewOption(fmt.Sprintf("%s — %s", h.Name, h.Target()), h.Name)
	}

	var selected []string
	err := huh.NewForm(huh.NewGroup(
		huh.NewMultiSelect[string]().
			Title("Importer depuis ~/.ssh/config").
			Description("espace : sélectionner · entrée : importer").
			Options(opts...).
			Value(&selected),
	)).Run()
	if err != nil {
		if errors.Is(err, huh.ErrUserAborted) {
			return nil, ErrAborted
		}
		return nil, err
	}

	hosts := make([]host.Host, 0, len(selected))
	for _, name := range selected {
		hosts = append(hosts, byName[name])
	}
	return hosts, nil
}