1// Package form provides interactive huh-based prompts for collecting user
2// input when keld is invoked via the interactive menu.
3package form
4
5import (
6 "errors"
7 "fmt"
8 "strings"
9
10 "charm.land/huh/v2"
11)
12
13// ErrAborted is returned when the user cancels a form (ctrl+c / q).
14var ErrAborted = errors.New("aborted by user")
15
16// wrapAbort converts huh's abort error into our own sentinel.
17func wrapAbort(err error) error {
18 if err != nil && errors.Is(err, huh.ErrUserAborted) {
19 return ErrAborted
20 }
21 return err
22}
23
24// SelectPreset displays an interactive selector for the given preset names.
25// Returns the selected preset, or "" if the user picks "(none)".
26func SelectPreset(presets []string) (string, error) {
27 if len(presets) == 0 {
28 return "", nil
29 }
30
31 opts := make([]huh.Option[string], 0, len(presets)+1)
32 opts = append(opts, huh.NewOption("(global defaults only)", ""))
33 for _, p := range presets {
34 opts = append(opts, huh.NewOption(p, p))
35 }
36
37 var selected string
38 form := huh.NewForm(
39 huh.NewGroup(
40 huh.NewSelect[string]().
41 Title("Select a preset").
42 Options(opts...).
43 Value(&selected),
44 ),
45 )
46
47 if err := wrapAbort(form.Run()); err != nil {
48 return "", err
49 }
50 return selected, nil
51}
52
53// TargetDirectory prompts for a restore target directory.
54func TargetDirectory() (string, error) {
55 var target string
56 form := huh.NewForm(
57 huh.NewGroup(
58 huh.NewInput().
59 Title("Target directory").
60 Placeholder("e.g. /tmp/restore").
61 Value(&target).
62 Validate(notEmpty("target directory")),
63 ),
64 )
65
66 if err := wrapAbort(form.Run()); err != nil {
67 return "", err
68 }
69 return target, nil
70}
71
72// BackupPaths collects one or more paths to back up when none are configured.
73func BackupPaths() ([]string, error) {
74 var raw string
75 form := huh.NewForm(
76 huh.NewGroup(
77 huh.NewInput().
78 Title("Paths to back up").
79 Description("Space-separated list of files or directories.").
80 Placeholder("e.g. /home/user /etc").
81 Value(&raw).
82 Validate(notEmpty("backup path")),
83 ),
84 )
85
86 if err := wrapAbort(form.Run()); err != nil {
87 return nil, err
88 }
89 return splitFields(raw), nil
90}
91
92// notEmpty returns a validation function that rejects blank or
93// whitespace-only input.
94func notEmpty(fieldName string) func(string) error {
95 return func(s string) error {
96 if strings.TrimSpace(s) == "" {
97 return fmt.Errorf("%s is required", fieldName)
98 }
99 return nil
100 }
101}
102
103// splitFields splits a string on whitespace.
104func splitFields(s string) []string {
105 return strings.Fields(s)
106}