commit efd56758e94db8d8728c9497c7d5ca8ca8098070
parent cd1a8ede280df57f8dafe24aadab756d939ed5fa
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Tue, 25 Aug 2026 21:59:14 -0700
Fix terminal color contrast
Diffstat:
4 files changed, 53 insertions(+), 18 deletions(-)
diff --git a/internal/config/config.go b/internal/config/config.go
@@ -7,7 +7,6 @@ import (
"os"
"path/filepath"
"runtime"
- "strconv"
"strings"
)
@@ -31,12 +30,11 @@ const (
)
type Config struct {
- BaseURL string `json:"baseUrl"`
- Theme Theme `json:"theme"`
- Token string `json:"token,omitempty"`
- LastTab FeedKind `json:"lastTab,omitempty"`
- NoColor bool `json:"-"`
- AutoLight bool `json:"-"`
+ BaseURL string `json:"baseUrl"`
+ Theme Theme `json:"theme"`
+ Token string `json:"token,omitempty"`
+ LastTab FeedKind `json:"lastTab,omitempty"`
+ NoColor bool `json:"-"`
}
func NormalizeBaseURL(value string) (string, error) {
@@ -135,8 +133,7 @@ func Load(env map[string]string) (Config, error) {
if !validTab(last) {
last = ""
}
- background, _ := strconv.Atoi(env["COLORFGBG"][strings.LastIndex(env["COLORFGBG"], ";")+1:])
- return Config{BaseURL: base, Theme: theme, Token: token, LastTab: last, NoColor: noColor, AutoLight: background > 8}, nil
+ return Config{BaseURL: base, Theme: theme, Token: token, LastTab: last, NoColor: noColor}, nil
}
func Save(cfg Config, env map[string]string) error {
path := ConfigPath(env)
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
@@ -198,13 +198,17 @@ func isolatedEnv(home string) map[string]string {
}
return env
}
-func TestLoadDetectsLightAutomaticTheme(t *testing.T) {
+func TestLoadDoesNotTrustColorFGBG(t *testing.T) {
home := t.TempDir()
- cfg, err := Load(map[string]string{"HOME": home, "XDG_CONFIG_HOME": home, "COLORFGBG": "0;15"})
+ light, err := Load(map[string]string{"HOME": home, "XDG_CONFIG_HOME": home, "COLORFGBG": "0;15"})
if err != nil {
t.Fatal(err)
}
- if cfg.Theme != ThemeAuto || !cfg.AutoLight {
- t.Fatalf("automatic theme detection = %#v", cfg)
+ dark, err := Load(map[string]string{"HOME": home, "XDG_CONFIG_HOME": home, "COLORFGBG": "15;0"})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if light != dark || light.Theme != ThemeAuto {
+ t.Fatalf("COLORFGBG changed automatic configuration: light=%#v dark=%#v", light, dark)
}
}
diff --git a/internal/tui/model_test.go b/internal/tui/model_test.go
@@ -9,6 +9,7 @@ import (
"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
"github.com/ryan/gotextlog/internal/config"
"github.com/ryan/gotextlog/internal/textlog"
)
@@ -263,3 +264,17 @@ func TestReferenceListKeepsSelectionVisible(t *testing.T) {
t.Fatalf("selected reference is outside the viewport:\n%s", view)
}
}
+func TestAutomaticThemeUsesTerminalPalette(t *testing.T) {
+ p := colors(config.ThemeAuto, false)
+ if p.ink != "" || p.accent != "2" || p.muted != "8" {
+ t.Fatalf("automatic palette uses fixed colors: %#v", p)
+ }
+}
+func TestAuthenticatedHeaderStaysOnOneLine(t *testing.T) {
+ model := New(&acceptanceBackend{}, config.Config{BaseURL: "https://example.test", Theme: config.ThemeAuto, Token: "token"}, Options{})
+ model.width = 65
+ model.me = &textlog.CurrentUser{Handle: "longusername"}
+ if height := lipgloss.Height(model.header(model.current(), colors(config.ThemeAuto, false))); height != 1 {
+ t.Fatalf("authenticated header height = %d, want 1", height)
+ }
+}
diff --git a/internal/tui/view.go b/internal/tui/view.go
@@ -13,20 +13,25 @@ import (
type palette struct{ ink, muted, soft, accent, danger, success, quote lipgloss.Color }
-func colors(theme config.Theme, noColor, autoLight bool) palette {
+func colors(theme config.Theme, noColor bool) palette {
if noColor {
return palette{}
}
- if theme == config.ThemeLight || (theme == config.ThemeAuto && autoLight) {
+ if theme == config.ThemeAuto {
+ // The terminal owns automatic colors. COLORFGBG is often stale inside
+ // tmux and can otherwise select dark text on a dark background.
+ return palette{"", "8", "8", "2", "1", "2", "8"}
+ }
+ if theme == config.ThemeLight {
return palette{"#20231f", "#747c72", "#d9dbd4", "#55734a", "#7a3f39", "#466342", "#e9e9e3"}
}
- return palette{"#e5e8e1", "#969d93", "#343a33", "#9abd8e", "#efb3aa", "#b9d5b2", "#222622"}
+ return palette{"#f4f7f2", "#a9b3a9", "#536053", "#a9d99c", "#ffb4ab", "#b8e0ae", "#293229"}
}
func (m Model) View() string {
if len(m.stack) == 0 {
return ""
}
- p := colors(m.config.Theme, m.config.NoColor, m.config.AutoLight)
+ p := colors(m.config.Theme, m.config.NoColor)
s := m.current()
header := m.header(s, p)
body := m.screenView(s, p)
@@ -47,11 +52,25 @@ func (m *Model) header(s *screen, p palette) string {
tabs := []string{"1 hot", "2 latest", "3 live"}
if m.config.Token != "" {
tabs = []string{"1 for you", "2 to me", "3 hot", "4 latest", "5 live"}
+ if m.width < 80 {
+ tabs = []string{"1 you", "2 me", "3 hot", "4 latest", "5 live"}
+ }
+ }
+ if m.width < 80 && s.kind != screenFeed && s.kind != screenLive {
+ tabs = nil
}
activeTab := strings.ReplaceAll(active, "-", " ")
+ activeNumber := ""
+ if s.kind == screenFeed || s.kind == screenLive {
+ if m.config.Token != "" {
+ activeNumber = map[string]string{"for-you": "1", "to-me": "2", "hot": "3", "latest": "4", "live": "5"}[active]
+ } else {
+ activeNumber = map[string]string{"hot": "1", "latest": "2", "live": "3"}[active]
+ }
+ }
for i, tab := range tabs {
style := lipgloss.NewStyle().Foreground(p.muted)
- if strings.Contains(tab, activeTab) {
+ if strings.Contains(tab, activeTab) || activeNumber != "" && strings.HasPrefix(tab, activeNumber+" ") {
style = style.Bold(true).Foreground(p.accent)
}
tabs[i] = style.Render(tab)