Replace double-pointer output params with return struct

Amolith created

Change summary

cmd/root.go | 107 ++++++++++++++++++++++++------------------------------
1 file changed, 47 insertions(+), 60 deletions(-)

Detailed changes

cmd/root.go 🔗

@@ -243,12 +243,9 @@ func runInteractive() (command, preset string, overrides map[string][]string, er
 		preset = presets[0]
 	}
 
-	// Screen references for extracting results after the session.
-	// These are populated by the resolve screen's builder function.
-	var snapshotScreen *screens.Snapshot
-	var filePickerScreen *screens.FilePicker
-	var targetScreen *screens.Target
-	var overwriteScreen *screens.Overwrite
+	// cmdScreens is populated by the resolve screen's builder function
+	// so the caller can extract results after the session completes.
+	var cmdScreens *commandScreens
 
 	// The resolve screen sits between menu/preset and command-specific
 	// screens. It resolves the config and dynamically builds the
@@ -260,8 +257,11 @@ func runInteractive() (command, preset string, overrides map[string][]string, er
 			p = presetScreen.Value()
 		}
 
-		return buildCommandScreens(cmd, p, &styles,
-			&snapshotScreen, &filePickerScreen, &targetScreen, &overwriteScreen)
+		cmdScreens = buildCommandScreens(cmd, p, &styles)
+		if cmdScreens == nil {
+			return nil
+		}
+		return cmdScreens.list
 	})
 	screenList = append(screenList, resolveScreen)
 
@@ -287,26 +287,30 @@ func runInteractive() (command, preset string, overrides map[string][]string, er
 	}
 
 	// Extract overrides from the completed command-specific screens.
-	overrides = extractCommandOverrides(command, snapshotScreen, filePickerScreen, targetScreen, overwriteScreen)
+	overrides = extractCommandOverrides(command, cmdScreens)
 
 	return command, preset, overrides, nil
 }
 
+// commandScreens holds the screens built for a command's interactive
+// flow along with typed references to each screen. This lets
+// runInteractive read results from the completed screens without
+// resorting to double-pointer output parameters.
+type commandScreens struct {
+	list       []ui.Screen
+	snapshot   *screens.Snapshot
+	filePicker *screens.FilePicker
+	target     *screens.Target
+	overwrite  *screens.Overwrite
+}
+
 // buildCommandScreens resolves the config for the given command and
 // preset, then builds the command-specific screens that need user
-// input. Screen pointers are stored in the provided output parameters
-// so the caller can extract results after the session completes.
-func buildCommandScreens(
-	command, preset string,
-	styles *theme.Styles,
-	snapshotOut **screens.Snapshot,
-	filePickerOut **screens.FilePicker,
-	targetOut **screens.Target,
-	overwriteOut **screens.Overwrite,
-) []ui.Screen {
+// input. Returns nil when the command needs no interactive screens.
+func buildCommandScreens(command, preset string, styles *theme.Styles) *commandScreens {
 	switch command {
 	case "restore":
-		return buildRestoreScreens(preset, styles, snapshotOut, filePickerOut, targetOut, overwriteOut)
+		return buildRestoreScreens(preset, styles)
 	default:
 		return nil
 	}
@@ -314,14 +318,7 @@ func buildCommandScreens(
 
 // buildRestoreScreens creates the restore flow screens, skipping any
 // that are already provided by the resolved config.
-func buildRestoreScreens(
-	preset string,
-	styles *theme.Styles,
-	snapshotOut **screens.Snapshot,
-	filePickerOut **screens.FilePicker,
-	targetOut **screens.Target,
-	overwriteOut **screens.Overwrite,
-) []ui.Screen {
+func buildRestoreScreens(preset string, styles *theme.Styles) *commandScreens {
 	cfg, err := config.Resolve(preset, "restore", nil)
 	if err != nil {
 		// Config resolution failed. Return no screens so the session
@@ -330,16 +327,15 @@ func buildRestoreScreens(
 		return nil
 	}
 
-	var list []ui.Screen
+	cs := &commandScreens{}
 
 	// Step 1: Snapshot ID (skip if already provided as an argument).
 	if len(cfg.Arguments) == 0 {
 		loader := screens.SnapshotLoader(func() ([]restic.Snapshot, error) {
 			return restic.ListSnapshots(cfg)
 		})
-		ss := screens.NewSnapshot(loader, styles)
-		*snapshotOut = ss
-		list = append(list, ss)
+		cs.snapshot = screens.NewSnapshot(loader, styles)
+		cs.list = append(cs.list, cs.snapshot)
 	}
 
 	// Step 2: File selection (skip if include flags set or snapshot
@@ -349,8 +345,8 @@ func buildRestoreScreens(
 		// Determine the snapshot ID source: from the snapshot screen
 		// if it exists, otherwise from the resolved config.
 		snapshotIDFn := func() string {
-			if *snapshotOut != nil {
-				return (*snapshotOut).Value()
+			if cs.snapshot != nil {
+				return cs.snapshot.Value()
 			}
 			if len(cfg.Arguments) > 0 {
 				return cfg.Arguments[0]
@@ -370,63 +366,54 @@ func buildRestoreScreens(
 			return restic.RunLs(cfg, snapshotID)
 		}
 
-		fps := screens.NewFilePicker(fileLoader, snapshotIDFn, styles)
-		*filePickerOut = fps
-		list = append(list, fps)
+		cs.filePicker = screens.NewFilePicker(fileLoader, snapshotIDFn, styles)
+		cs.list = append(cs.list, cs.filePicker)
 	}
 
 	// Step 3: Target directory (skip if already set).
 	if !cfg.HasFlag("target") {
-		ts := screens.NewTarget(styles)
-		*targetOut = ts
-		list = append(list, ts)
+		cs.target = screens.NewTarget(styles)
+		cs.list = append(cs.list, cs.target)
 	}
 
 	// Step 4: Overwrite behaviour (skip if already set).
 	if !cfg.HasFlag("overwrite") {
-		ows := screens.NewOverwrite(styles)
-		*overwriteOut = ows
-		list = append(list, ows)
+		cs.overwrite = screens.NewOverwrite(styles)
+		cs.list = append(cs.list, cs.overwrite)
 	}
 
-	return list
+	return cs
 }
 
 // extractCommandOverrides reads the completed screen values and
 // returns a map suitable for passing to runCommand as overrides.
-func extractCommandOverrides(
-	command string,
-	snapshotScreen *screens.Snapshot,
-	filePickerScreen *screens.FilePicker,
-	targetScreen *screens.Target,
-	overwriteScreen *screens.Overwrite,
-) map[string][]string {
-	if command != "restore" {
+func extractCommandOverrides(command string, cs *commandScreens) map[string][]string {
+	if command != "restore" || cs == nil {
 		return nil
 	}
 
 	overrides := make(map[string][]string)
 
-	if snapshotScreen != nil {
-		if v := snapshotScreen.Value(); v != "" {
+	if cs.snapshot != nil {
+		if v := cs.snapshot.Value(); v != "" {
 			overrides[overrideArgumentsKey] = []string{v}
 		}
 	}
 
-	if filePickerScreen != nil {
-		if includes := filePickerScreen.Includes(); len(includes) > 0 {
+	if cs.filePicker != nil {
+		if includes := cs.filePicker.Includes(); len(includes) > 0 {
 			overrides["include"] = includes
 		}
 	}
 
-	if targetScreen != nil {
-		if v := targetScreen.Value(); v != "" {
+	if cs.target != nil {
+		if v := cs.target.Value(); v != "" {
 			overrides["target"] = []string{v}
 		}
 	}
 
-	if overwriteScreen != nil {
-		if v := overwriteScreen.Value(); v != "" {
+	if cs.overwrite != nil {
+		if v := cs.overwrite.Value(); v != "" {
 			overrides["overwrite"] = []string{v}
 		}
 	}