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
}