From 9e7dbdb9be0a321541d23ef152c9ab7ff83f78d6 Mon Sep 17 00:00:00 2001 From: David Gageot Date: Tue, 17 Mar 2026 13:14:43 +0100 Subject: [PATCH 1/3] Properly configured tmux don't need this Signed-off-by: David Gageot --- pkg/tui/components/spinner/spinner.go | 33 +-------------------------- 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/pkg/tui/components/spinner/spinner.go b/pkg/tui/components/spinner/spinner.go index f456ef06a..01e813f32 100644 --- a/pkg/tui/components/spinner/spinner.go +++ b/pkg/tui/components/spinner/spinner.go @@ -2,7 +2,6 @@ package spinner import ( "math/rand/v2" - "os" "strings" tea "charm.land/bubbletea/v2" @@ -138,37 +137,7 @@ func (s *spinner) Stop() { } // spinnerFrames holds the animation frames for the current terminal. -// Braille characters are used by default; inside tmux they don't render -// correctly, so we fall back to ASCII. -var spinnerFrames = selectFrames(inMultiplexer()) - -// inMultiplexer reports whether the process is running inside a terminal -// multiplexer (tmux, screen). Detection checks multiple env vars because -// some of them may be stripped in containers or sudo sessions. -func inMultiplexer() bool { - if os.Getenv("TMUX") != "" { - return true - } - if os.Getenv("STY") != "" { // GNU screen - return true - } - term := os.Getenv("TERM") - return strings.HasPrefix(term, "tmux") || strings.HasPrefix(term, "screen") -} - -var ( - brailleFrames = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"} - asciiFrames = []string{"|", "/", "-", "\\"} -) - -// selectFrames returns ASCII spinner frames when inTmux is true, -// braille frames otherwise. -func selectFrames(inTmux bool) []string { - if inTmux { - return asciiFrames - } - return brailleFrames -} +var spinnerFrames = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"} // Frame returns the spinner character for the given animation frame. func Frame(index int) string { From 8e759da5f6e2618c08957c1d5b337c07408d5292 Mon Sep 17 00:00:00 2001 From: David Gageot Date: Tue, 17 Mar 2026 13:16:16 +0100 Subject: [PATCH 2/3] Properly configured tmux don't need this Signed-off-by: David Gageot --- cmd/root/new.go | 23 +--------------- pkg/tui/components/spinner/spinner_test.go | 32 ---------------------- 2 files changed, 1 insertion(+), 54 deletions(-) diff --git a/cmd/root/new.go b/cmd/root/new.go index af1be9974..2c5a45cf3 100644 --- a/cmd/root/new.go +++ b/cmd/root/new.go @@ -108,15 +108,7 @@ func runTUI(ctx context.Context, rt runtime.Runtime, sess *session.Session, spaw wd, _ := os.Getwd() model := tui.New(ctx, spawner, a, wd, cleanup) - programOpts := []tea.ProgramOption{tea.WithContext(ctx), tea.WithFilter(filter)} - - // Terminal multiplexers can drop frames at the default 60 FPS, making - // spinners appear static. Cap the renderer at 30 FPS when detected. - if inMultiplexer() { - programOpts = append(programOpts, tea.WithFPS(30)) - } - - p := tea.NewProgram(model, programOpts...) + p := tea.NewProgram(model, tea.WithContext(ctx), tea.WithFilter(filter)) coalescer.SetSender(p.Send) if m, ok := model.(interface{ SetProgram(p *tea.Program) }); ok { @@ -126,16 +118,3 @@ func runTUI(ctx context.Context, rt runtime.Runtime, sess *session.Session, spaw _, err := p.Run() return err } - -// inMultiplexer reports whether the process is running inside a terminal -// multiplexer (tmux, screen). -func inMultiplexer() bool { - if os.Getenv("TMUX") != "" { - return true - } - if os.Getenv("STY") != "" { - return true - } - term := os.Getenv("TERM") - return strings.HasPrefix(term, "tmux") || strings.HasPrefix(term, "screen") -} diff --git a/pkg/tui/components/spinner/spinner_test.go b/pkg/tui/components/spinner/spinner_test.go index 3eb52126d..93c669af1 100644 --- a/pkg/tui/components/spinner/spinner_test.go +++ b/pkg/tui/components/spinner/spinner_test.go @@ -28,38 +28,6 @@ func TestFrameWrapsAround(t *testing.T) { require.Equal(t, spinnerFrames[0], Frame(n), "Frame should wrap around") } -func TestSelectFrames(t *testing.T) { - require.Equal(t, brailleFrames, selectFrames(false)) - require.Equal(t, asciiFrames, selectFrames(true)) -} - -func TestInMultiplexer(t *testing.T) { - tests := []struct { - name string - env map[string]string - want bool - }{ - {"plain terminal", map[string]string{"TERM": "xterm-256color"}, false}, - {"TMUX set", map[string]string{"TMUX": "/tmp/tmux-1000/default,123,0"}, true}, - {"STY set", map[string]string{"STY": "12345.pts-0"}, true}, - {"TERM=tmux-256color", map[string]string{"TERM": "tmux-256color"}, true}, - {"TERM=screen-256color", map[string]string{"TERM": "screen-256color"}, true}, - {"TERM=screen", map[string]string{"TERM": "screen"}, true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Clear all multiplexer env vars - t.Setenv("TMUX", "") - t.Setenv("STY", "") - t.Setenv("TERM", "") - for k, v := range tt.env { - t.Setenv(k, v) - } - require.Equal(t, tt.want, inMultiplexer()) - }) - } -} - func BenchmarkSpinner_ModeSpinnerOnly(b *testing.B) { s := New(ModeSpinnerOnly, lipgloss.NewStyle()) for b.Loop() { From 8fb16e8b04d4890af8279b62482c59a86d6dc2ce Mon Sep 17 00:00:00 2001 From: David Gageot Date: Tue, 17 Mar 2026 13:41:37 +0100 Subject: [PATCH 3/3] Best possible rendering in ghostty/tmux Signed-off-by: David Gageot --- pkg/tui/components/scrollbar/scrollbar.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/tui/components/scrollbar/scrollbar.go b/pkg/tui/components/scrollbar/scrollbar.go index 45863362c..1eaac1458 100644 --- a/pkg/tui/components/scrollbar/scrollbar.go +++ b/pkg/tui/components/scrollbar/scrollbar.go @@ -35,7 +35,7 @@ func New() *Model { return &Model{ width: Width, trackChar: "⎪", - thumbChar: "┃", + thumbChar: "⎪", } }