diff --git a/pkg/styles/theme.go b/pkg/styles/theme.go index 504db7475fd..8a4be34d989 100644 --- a/pkg/styles/theme.go +++ b/pkg/styles/theme.go @@ -48,6 +48,7 @@ package styles import ( "os" + "runtime" lipgloss "charm.land/lipgloss/v2" "charm.land/lipgloss/v2/compat" @@ -59,8 +60,36 @@ func configureLipglossCompat() { compat.Profile = colorprofile.Detect(os.Stderr, os.Environ()) } +func shouldConfigureLipglossCompat(goos string, stderrMode os.FileMode, statErr error) bool { + // On Windows, querying terminal capabilities against redirected/pipe handles + // can hang under some wrapper environments. Skip startup probing unless stderr + // is attached to a character device. + if goos == "windows" { + if statErr != nil { + return false + } + return stderrMode&(os.ModeDevice|os.ModeCharDevice) == (os.ModeDevice | os.ModeCharDevice) + } + return true +} + func init() { - configureLipglossCompat() + stderrInfo, statErr := os.Stderr.Stat() + // Defensive fallback: Stat should not normally return (nil, nil). Treat that + // impossible state as an invalid stderr handle so Windows startup probing is + // safely skipped. + if statErr == nil && stderrInfo == nil { + statErr = os.ErrInvalid + } + // Zero mode means "unknown/unset"; on Windows this keeps the startup probe + // disabled unless stderr is explicitly confirmed as a character device. + stderrMode := os.FileMode(0) + if stderrInfo != nil { + stderrMode = stderrInfo.Mode() + } + if shouldConfigureLipglossCompat(runtime.GOOS, stderrMode, statErr) { + configureLipglossCompat() + } } // Hex color constants for light and dark variants. diff --git a/pkg/styles/theme_test.go b/pkg/styles/theme_test.go index d797cb6e8a2..583d0d09a36 100644 --- a/pkg/styles/theme_test.go +++ b/pkg/styles/theme_test.go @@ -280,6 +280,63 @@ func TestConfigureLipglossCompatUsesStderr(t *testing.T) { } } +func TestShouldConfigureLipglossCompat(t *testing.T) { + tests := []struct { + name string + goos string + stderrMode os.FileMode + statErr error + want bool + }{ + { + name: "windows character device", + goos: "windows", + stderrMode: os.ModeDevice | os.ModeCharDevice, + want: true, + }, + { + name: "windows redirected pipe", + goos: "windows", + stderrMode: os.ModeNamedPipe, + want: false, + }, + { + name: "windows zero mode stat success", + goos: "windows", + stderrMode: os.FileMode(0), + want: false, + }, + { + name: "windows stat error", + goos: "windows", + statErr: os.ErrPermission, + want: false, + }, + { + name: "windows ErrInvalid fallback", + goos: "windows", + statErr: os.ErrInvalid, + want: false, + }, + { + name: "non-windows still configures", + goos: "linux", + stderrMode: os.ModeNamedPipe, + statErr: os.ErrPermission, + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := shouldConfigureLipglossCompat(tt.goos, tt.stderrMode, tt.statErr) + if got != tt.want { + t.Fatalf("shouldConfigureLipglossCompat(%q, %v, %v) = %v, want %v", tt.goos, tt.stderrMode, tt.statErr, got, tt.want) + } + }) + } +} + // TestBordersExist verifies that all expected border definitions are defined func TestBordersExist(t *testing.T) { borders := map[string]lipgloss.Border{