Skip interactive prompts when preset already provides the values

Amolith created

Change summary

cmd/root.go               | 54 +++++++++++++++++++++++++++++-----------
internal/config/config.go | 12 +++++++++
internal/form/form.go     | 38 ++++++++++++++++------------
3 files changed, 73 insertions(+), 31 deletions(-)

Detailed changes

cmd/root.go 🔗

@@ -107,7 +107,14 @@ var rootCmd = &cobra.Command{
 			}
 			preset = p
 
-			cmdOverrides, err := promptForCommand(command)
+			// Resolve config before prompting so we can see what the
+			// preset already provides and skip unnecessary prompts.
+			peek, err := config.Resolve(preset, command, overrides)
+			if err != nil {
+				return err
+			}
+
+			cmdOverrides, err := promptForCommand(command, peek)
 			if err != nil {
 				return err
 			}
@@ -350,34 +357,51 @@ func promptPreset() (string, error) {
 }
 
 // promptForCommand collects required inputs for commands that need them.
+// The resolved config is checked first so prompts are skipped for values
+// the preset already provides.
 // Returns CLI overrides to merge, or nil if the command needs no extra input.
-func promptForCommand(command string) (map[string][]string, error) {
+func promptForCommand(command string, cfg *config.ResolvedConfig) (map[string][]string, error) {
 	switch command {
 	case "restore":
-		return promptRestore()
+		return promptRestore(cfg)
 	case "backup":
-		return promptBackup()
+		return promptBackup(cfg)
 	default:
 		return nil, nil
 	}
 }
 
-// promptRestore collects the snapshot ID and target directory for restore.
-func promptRestore() (map[string][]string, error) {
-	snapshotID, target, err := form.RestoreInputs()
+// promptRestore collects the snapshot ID and target directory for restore,
+// skipping prompts for values already present in the resolved config.
+func promptRestore(cfg *config.ResolvedConfig) (map[string][]string, error) {
+	hasSnapshotID := len(cfg.Arguments) > 0
+	hasTarget := cfg.HasFlag("target")
+
+	if hasSnapshotID && hasTarget {
+		return nil, nil
+	}
+
+	snapshotID, target, err := form.RestoreInputs(hasSnapshotID, hasTarget)
 	if err != nil {
 		return nil, fmt.Errorf("restore inputs: %w", err)
 	}
-	return map[string][]string{
-		overrideArgumentsKey: {snapshotID},
-		"target":             {target},
-	}, nil
+
+	overrides := make(map[string][]string)
+	if !hasSnapshotID {
+		overrides[overrideArgumentsKey] = []string{snapshotID}
+	}
+	if !hasTarget {
+		overrides["target"] = []string{target}
+	}
+	return overrides, nil
 }
 
-// promptBackup collects backup paths if none are likely to come from config.
-// The user is always offered the chance to provide paths; config-defined
-// _arguments will still take precedence during resolution.
-func promptBackup() (map[string][]string, error) {
+// promptBackup collects backup paths when none are configured in the preset.
+func promptBackup(cfg *config.ResolvedConfig) (map[string][]string, error) {
+	if len(cfg.Arguments) > 0 {
+		return nil, nil
+	}
+
 	paths, err := form.BackupPaths()
 	if err != nil {
 		return nil, fmt.Errorf("backup paths: %w", err)

internal/config/config.go 🔗

@@ -51,6 +51,18 @@ type Flag struct {
 	Value string // empty for boolean switches
 }
 
+// HasFlag reports whether the resolved config contains a flag with the given
+// name (without leading dashes, e.g. "target" not "--target").
+func (rc *ResolvedConfig) HasFlag(name string) bool {
+	dashed := flagName(name)
+	for _, f := range rc.Flags {
+		if f.Name == dashed {
+			return true
+		}
+	}
+	return false
+}
+
 // rawConfig is the entire parsed TOML file as nested string-keyed maps.
 type rawConfig map[string]any
 

internal/form/form.go 🔗

@@ -50,23 +50,29 @@ func SelectPreset(presets []string) (string, error) {
 }
 
 // RestoreInputs collects the required inputs for `restic restore`:
-// a snapshot ID and a target directory.
-func RestoreInputs() (snapshotID, target string, err error) {
-	form := huh.NewForm(
-		huh.NewGroup(
-			huh.NewInput().
-				Title("Snapshot ID").
-				Placeholder("e.g. latest or a1b2c3d4").
-				Value(&snapshotID).
-				Validate(notEmpty("snapshot ID")),
-			huh.NewInput().
-				Title("Target directory").
-				Placeholder("e.g. /tmp/restore").
-				Value(&target).
-				Validate(notEmpty("target directory")),
-		),
-	)
+// a snapshot ID and a target directory. Fields whose corresponding
+// "has" parameter is true are skipped (already provided by config).
+func RestoreInputs(hasSnapshotID, hasTarget bool) (snapshotID, target string, err error) {
+	var fields []huh.Field
+	if !hasSnapshotID {
+		fields = append(fields, huh.NewInput().
+			Title("Snapshot ID").
+			Placeholder("e.g. latest or a1b2c3d4").
+			Value(&snapshotID).
+			Validate(notEmpty("snapshot ID")))
+	}
+	if !hasTarget {
+		fields = append(fields, huh.NewInput().
+			Title("Target directory").
+			Placeholder("e.g. /tmp/restore").
+			Value(&target).
+			Validate(notEmpty("target directory")))
+	}
+	if len(fields) == 0 {
+		return "", "", nil
+	}
 
+	form := huh.NewForm(huh.NewGroup(fields...))
 	if err := wrapAbort(form.Run()); err != nil {
 		return "", "", err
 	}