fix(noninteractive): spinner text on light backgrounds

Christian Rocha created

Change summary

internal/agent/agent.go    |  2 +-
internal/app/app.go        | 25 ++++++++++++++++++++++++-
internal/format/spinner.go | 23 +++++------------------
3 files changed, 30 insertions(+), 20 deletions(-)

Detailed changes

internal/agent/agent.go 🔗

@@ -286,7 +286,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*fantasy
 			// Strip leading newline from initial text content. This is is
 			// particularly important in non-interactive mode where leading
 			// newlines are very visible.
-			if len(currentAssistant.Parts) == 0 && strings.HasPrefix(text, "\n") {
+			if len(currentAssistant.Parts) == 0 {
 				text = strings.TrimPrefix(text, "\n")
 			}
 

internal/app/app.go 🔗

@@ -9,6 +9,7 @@ import (
 	"fmt"
 	"io"
 	"log/slog"
+	"os"
 	"sync"
 	"time"
 
@@ -27,7 +28,11 @@ import (
 	"github.com/charmbracelet/crush/internal/permission"
 	"github.com/charmbracelet/crush/internal/pubsub"
 	"github.com/charmbracelet/crush/internal/session"
+	"github.com/charmbracelet/crush/internal/tui/components/anim"
+	"github.com/charmbracelet/crush/internal/tui/styles"
+	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/charmbracelet/x/ansi"
+	"github.com/charmbracelet/x/exp/charmtone"
 )
 
 type App struct {
@@ -114,7 +119,25 @@ func (app *App) RunNonInteractive(ctx context.Context, output io.Writer, prompt
 
 	var spinner *format.Spinner
 	if !quiet {
-		spinner = format.NewSpinner(ctx, cancel, "Generating")
+		t := styles.CurrentTheme()
+
+		// Detect background color to set the appropriate color for the
+		// spinner's 'Generating...' text. Without this, that text would be
+		// unreadable in light terminals.
+		hasDarkBG := true
+		if f, ok := output.(*os.File); ok {
+			hasDarkBG = lipgloss.HasDarkBackground(os.Stdin, f)
+		}
+		defaultFG := lipgloss.LightDark(hasDarkBG)(charmtone.Pepper, t.FgBase)
+
+		spinner = format.NewSpinner(ctx, cancel, anim.Settings{
+			Size:        10,
+			Label:       "Generating",
+			LabelColor:  defaultFG,
+			GradColorA:  t.Primary,
+			GradColorB:  t.Secondary,
+			CycleColors: true,
+		})
 		spinner.Start()
 	}
 

internal/format/spinner.go 🔗

@@ -8,7 +8,6 @@ import (
 
 	tea "github.com/charmbracelet/bubbletea/v2"
 	"github.com/charmbracelet/crush/internal/tui/components/anim"
-	"github.com/charmbracelet/crush/internal/tui/styles"
 	"github.com/charmbracelet/x/ansi"
 )
 
@@ -42,28 +41,16 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 }
 
 // NewSpinner creates a new spinner with the given message
-func NewSpinner(ctx context.Context, cancel context.CancelFunc, message string) *Spinner {
-	t := styles.CurrentTheme()
-	model := model{
-		anim: anim.New(anim.Settings{
-			Size:        10,
-			Label:       message,
-			LabelColor:  t.FgBase,
-			GradColorA:  t.Primary,
-			GradColorB:  t.Secondary,
-			CycleColors: true,
-		}),
+func NewSpinner(ctx context.Context, cancel context.CancelFunc, animSettings anim.Settings) *Spinner {
+	m := model{
+		anim:   anim.New(animSettings),
 		cancel: cancel,
 	}
 
-	prog := tea.NewProgram(
-		model,
-		tea.WithOutput(os.Stderr),
-		tea.WithContext(ctx),
-	)
+	p := tea.NewProgram(m, tea.WithOutput(os.Stderr), tea.WithContext(ctx))
 
 	return &Spinner{
-		prog: prog,
+		prog: p,
 		done: make(chan struct{}, 1),
 	}
 }