diff --git a/internal/config/config.go b/internal/config/config.go index 0f5490b..7128746 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -17,7 +17,6 @@ import ( "charm.land/glamour/v2" "charm.land/glamour/v2/styles" "charm.land/lipgloss/v2" - "github.com/charmbracelet/x/term" "go.aykhans.me/sarin/internal/sarin" "go.aykhans.me/sarin/internal/script" "go.aykhans.me/sarin/internal/types" @@ -254,7 +253,7 @@ func (config Config) Print() bool { } // Pipe mode: output raw content directly - if !term.IsTerminal(os.Stdout.Fd()) { + if !sarin.IsInteractiveTerminal(os.Stdout.Fd()) { fmt.Println(string(configYAML)) os.Exit(0) } diff --git a/internal/sarin/runner.go b/internal/sarin/runner.go index c704620..178be54 100644 --- a/internal/sarin/runner.go +++ b/internal/sarin/runner.go @@ -51,6 +51,18 @@ func SplitLogLevels(levels string) []string { return out } +// IsInteractiveTerminal reports whether fd is a terminal that can actually be +// drawn on. A pty reports as a terminal even while its size is still zero, and +// Bubble Tea paints into a width x height cell buffer, so a zero dimension +// renders nothing at all. +func IsInteractiveTerminal(fd uintptr) bool { + if !term.IsTerminal(fd) { + return false + } + width, height, err := term.GetSize(fd) + return err == nil && width > 0 && height > 0 +} + // respLogger logs a single completed response. type respLogger func(duration time.Duration, resp *fasthttp.Response) @@ -302,7 +314,7 @@ func (s sarin) Start(ctx context.Context, stopCtrl *StopController) { totalRequests = *s.totalRequests } - onTerminal := term.IsTerminal(os.Stdout.Fd()) + onTerminal := IsInteractiveTerminal(os.Stdout.Fd()) // The progress bar needs an interactive terminal to render. showProgressBar := s.showProgress && onTerminal // The bubbletea TUI hosts the bar and/or the live log box, so it runs whenever