feat: show progress bar on boot for feedback (#1371)

Andrey Nering and Ayman Bagabas created

Co-authored-by: Ayman Bagabas <ayman.bagabas@gmail.com>

Change summary

internal/app/app.go   | 14 ++++++++++----
internal/cmd/root.go  | 15 ++++++++++++---
internal/term/term.go | 15 +++++++++++++++
3 files changed, 37 insertions(+), 7 deletions(-)

Detailed changes

internal/app/app.go 🔗

@@ -28,6 +28,7 @@ import (
 	"github.com/charmbracelet/crush/internal/permission"
 	"github.com/charmbracelet/crush/internal/pubsub"
 	"github.com/charmbracelet/crush/internal/session"
+	"github.com/charmbracelet/crush/internal/term"
 	"github.com/charmbracelet/crush/internal/tui/components/anim"
 	"github.com/charmbracelet/crush/internal/tui/styles"
 	"github.com/charmbracelet/lipgloss/v2"
@@ -191,9 +192,12 @@ func (app *App) RunNonInteractive(ctx context.Context, output io.Writer, prompt
 
 	messageEvents := app.Messages.Subscribe(ctx)
 	messageReadBytes := make(map[string]int)
+	supportsProgressBar := term.SupportsProgressBar()
 
 	defer func() {
-		_, _ = fmt.Printf(ansi.ResetProgressBar)
+		if supportsProgressBar {
+			_, _ = fmt.Fprintf(os.Stderr, 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.
@@ -201,9 +205,11 @@ func (app *App) RunNonInteractive(ctx context.Context, output io.Writer, prompt
 	}()
 
 	for {
-		// 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)
+		if supportsProgressBar {
+			// HACK: Reinitialize the terminal progress bar on every iteration so
+			// it doesn't get hidden by the terminal due to inactivity.
+			_, _ = fmt.Fprintf(os.Stderr, ansi.SetIndeterminateProgressBar)
+		}
 
 		select {
 		case result := <-done:

internal/cmd/root.go 🔗

@@ -18,11 +18,13 @@ import (
 	"github.com/charmbracelet/crush/internal/config"
 	"github.com/charmbracelet/crush/internal/db"
 	"github.com/charmbracelet/crush/internal/event"
+	termutil "github.com/charmbracelet/crush/internal/term"
 	"github.com/charmbracelet/crush/internal/tui"
 	"github.com/charmbracelet/crush/internal/version"
 	"github.com/charmbracelet/fang"
 	"github.com/charmbracelet/lipgloss/v2"
 	uv "github.com/charmbracelet/ultraviolet"
+	"github.com/charmbracelet/x/ansi"
 	"github.com/charmbracelet/x/exp/charmtone"
 	"github.com/charmbracelet/x/term"
 	"github.com/spf13/cobra"
@@ -32,7 +34,6 @@ func init() {
 	rootCmd.PersistentFlags().StringP("cwd", "c", "", "Current working directory")
 	rootCmd.PersistentFlags().StringP("data-dir", "D", "", "Custom crush data directory")
 	rootCmd.PersistentFlags().BoolP("debug", "d", false, "Debug")
-
 	rootCmd.Flags().BoolP("help", "h", false, "Help")
 	rootCmd.Flags().BoolP("yolo", "y", false, "Automatically accept all permissions (dangerous mode)")
 
@@ -74,7 +75,7 @@ crush run "Explain the use of context in Go"
 crush -y
   `,
 	RunE: func(cmd *cobra.Command, args []string) error {
-		app, err := setupApp(cmd)
+		app, err := setupAppWithProgressBar(cmd)
 		if err != nil {
 			return err
 		}
@@ -92,7 +93,6 @@ crush -y
 			tea.WithEnvironment(env),
 			tea.WithContext(cmd.Context()),
 			tea.WithFilter(tui.MouseEventFilter)) // Filter mouse events based on focus state
-
 		go app.Subscribe(program)
 
 		if _, err := program.Run(); err != nil {
@@ -150,6 +150,15 @@ func Execute() {
 	}
 }
 
+func setupAppWithProgressBar(cmd *cobra.Command) (*app.App, error) {
+	if termutil.SupportsProgressBar() {
+		_, _ = fmt.Fprintf(os.Stderr, ansi.SetIndeterminateProgressBar)
+		defer func() { _, _ = fmt.Fprintf(os.Stderr, ansi.ResetProgressBar) }()
+	}
+
+	return setupApp(cmd)
+}
+
 // setupApp handles the common setup logic for both interactive and non-interactive modes.
 // It returns the app instance, config, cleanup function, and any error.
 func setupApp(cmd *cobra.Command) (*app.App, error) {

internal/term/term.go 🔗

@@ -0,0 +1,15 @@
+package term
+
+import (
+	"os"
+	"strings"
+)
+
+// SupportsProgressBar tries to determine whether the current terminal supports
+// progress bars by looking into environment variables.
+func SupportsProgressBar() bool {
+	termProg := os.Getenv("TERM_PROGRAM")
+	_, isWindowsTerminal := os.LookupEnv("WT_SESSION")
+
+	return isWindowsTerminal || strings.Contains(strings.ToLower(termProg), "ghostty")
+}