fix(noninteractive): always print newline after output

Christian Rocha created

This is particularly important to keep the last line of output from
being overwritten by the prompt when output's a TTY.

Change summary

internal/app/app.go | 20 ++++++++++++++------
internal/cmd/run.go |  7 ++++++-
2 files changed, 20 insertions(+), 7 deletions(-)

Detailed changes

internal/app/app.go 🔗

@@ -101,8 +101,8 @@ func (app *App) Config() *config.Config {
 	return app.config
 }
 
-// RunNonInteractive handles the execution flow when a prompt is provided via
-// CLI flag.
+// RunNonInteractive runs the application in non-interactive mode with the
+// given prompt, printing to stdout.
 func (app *App) RunNonInteractive(ctx context.Context, prompt string, quiet bool) error {
 	slog.Info("Running in non-interactive mode")
 
@@ -165,11 +165,19 @@ func (app *App) RunNonInteractive(ctx context.Context, prompt string, quiet bool
 	messageEvents := app.Messages.Subscribe(ctx)
 	messageReadBytes := make(map[string]int)
 
-	defer fmt.Printf(ansi.ResetProgressBar)
+	defer func() {
+		_, _ = fmt.Printf(ansi.ResetProgressBar)
+
+		// Always print a newline at the end. If output is a TTY this will
+		// prevent the prompt from overwriting the last line of output.
+		_, _ = fmt.Print('\n')
+	}()
+
 	for {
-		// HACK: add it again on every iteration so it doesn't get hidden by
-		// the terminal due to inactivity.
-		fmt.Printf(ansi.SetIndeterminateProgressBar)
+		// HACK: Reinitialize the terminal progress bar on every iteration so
+		// it doesn't get hidden by the terminal due to inactivity.
+		_, _ = fmt.Printf(ansi.SetIndeterminateProgressBar)
+
 		select {
 		case result := <-done:
 			stopSpinner()

internal/cmd/run.go 🔗

@@ -48,7 +48,12 @@ crush run -q "Generate a README for this project"
 			return fmt.Errorf("no prompt provided")
 		}
 
-		// Run non-interactive flow using the App method
+		// TODO: Make this work when redirected to something other than stdout.
+		// For example:
+		//     crush run "Do something fancy" > output.txt
+		//     echo "Do something fancy" | crush run > output.txt
+		//
+		// TODO: We currently need to press ^c twice to cancel. Fix that.
 		return app.RunNonInteractive(cmd.Context(), prompt, quiet)
 	},
 }