gotextlog

gotextlog
git clone https://git.ryansepassi.com/git/gotextlog.git
Log | Files | Refs

commit efd56758e94db8d8728c9497c7d5ca8ca8098070
parent cd1a8ede280df57f8dafe24aadab756d939ed5fa
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Tue, 25 Aug 2026 21:59:14 -0700

Fix terminal color contrast

Diffstat:
Minternal/config/config.go | 15++++++---------
Minternal/config/config_test.go | 12++++++++----
Minternal/tui/model_test.go | 15+++++++++++++++
Minternal/tui/view.go | 29++++++++++++++++++++++++-----
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)